diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 807ab3ee3500..0b5c0029e05e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -16,7 +16,7 @@ ######################### /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 +/hapi/hedera-protobufs/services @hashgraph/hedera-services @hashgraph/hedera-smart-contracts-core @jsync-swirlds @hashgraph/mirror-node ######################### diff --git a/.github/workflows/config/node-release.yaml b/.github/workflows/config/node-release.yaml index fd24a53f35db..18a52d366e68 100644 --- a/.github/workflows/config/node-release.yaml +++ b/.github/workflows/config/node-release.yaml @@ -1,11 +1,11 @@ release: branching: execution: - time: "20:00:00" + time: "18:00:00" schedule: - - on: "2024-11-22" - name: release/0.57 + - on: "2024-12-13" + name: release/0.58 initial-tag: create: true - name: v0.57.0-alpha.0 + name: v0.58.0-alpha.0 diff --git a/.github/workflows/node-flow-deploy-release-artifact.yaml b/.github/workflows/node-flow-deploy-release-artifact.yaml index 836f754e54f0..50e13ca7da0d 100644 --- a/.github/workflows/node-flow-deploy-release-artifact.yaml +++ b/.github/workflows/node-flow-deploy-release-artifact.yaml @@ -201,7 +201,7 @@ jobs: - name: Checkout Hedera Protobufs Code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: - token: ${{ secrets.GH_ACCESS_TOKEN }} + token: ${{ secrets.PROTOBUFS_GH_ACCESS_TOKEN }} fetch-depth: '0' repository: hashgraph/hedera-protobufs path: hedera-protobufs @@ -220,8 +220,8 @@ jobs: id: gpg_import uses: step-security/ghaction-import-gpg@6c8fe4d0126a59d57c21f87c9ae5dd3451fa3cca # v6.1.0 with: - gpg_private_key: ${{ secrets.SVCS_GPG_KEY_CONTENTS }} - passphrase: ${{ secrets.SVCS_GPG_KEY_PASSPHRASE }} + gpg_private_key: ${{ secrets.PROTOBUFS_GPG_KEY_CONTENTS }} + passphrase: ${{ secrets.PROTOBUFS_GPG_KEY_PASSPHRASE }} git_user_signingkey: true git_commit_gpgsign: true git_tag_gpgsign: true @@ -231,7 +231,7 @@ jobs: with: cwd: 'hedera-protobufs' author_name: swirlds-eng-automation - author_email: ${{ secrets.SVCS_GIT_USER_EMAIL }} + author_email: ${{ secrets.PROTOBUFS_GPG_USER_EMAIL }} commit: --signoff message: "ci: Copied recent protobuf changes from hedera-services" new_branch: "update-recent-protobuf-changes-${{ github.run_number }}" diff --git a/.github/workflows/node-zxc-build-release-artifact.yaml b/.github/workflows/node-zxc-build-release-artifact.yaml index 9c4eb42d900a..3d1fe85f9667 100644 --- a/.github/workflows/node-zxc-build-release-artifact.yaml +++ b/.github/workflows/node-zxc-build-release-artifact.yaml @@ -405,7 +405,7 @@ jobs: uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0 - name: Setup Docker Buildx Support - uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1 + uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0 with: version: v0.16.2 driver-opts: network=host @@ -560,7 +560,7 @@ jobs: service_account: "swirlds-automation@hedera-registry.iam.gserviceaccount.com" - name: Setup JFrog CLI - uses: jfrog/setup-jfrog-cli@96153976e4e81b3701e9cc0a5b9597e80614af81 # v4.5.1 + uses: jfrog/setup-jfrog-cli@dff217c085c17666e8849ebdbf29c8fe5e3995e6 # v4.5.2 env: JF_URL: ${{ secrets.jf-url }} JF_ACCESS_TOKEN: ${{ secrets.jf-access-token }} diff --git a/.github/workflows/node-zxcron-release-fsts-regression.yaml b/.github/workflows/node-zxcron-release-fsts-regression.yaml index 5b062f275bf1..439de9d9064b 100644 --- a/.github/workflows/node-zxcron-release-fsts-regression.yaml +++ b/.github/workflows/node-zxcron-release-fsts-regression.yaml @@ -57,7 +57,7 @@ jobs: major="${BASH_REMATCH[1]}" minor="${BASH_REMATCH[2]}" - if [[ "${major}" -eq 0 && "${minor}" -lt 55 ]]; then + if [[ "${major}" -eq 0 && "${minor}" -lt 57 ]]; then continue fi diff --git a/.github/workflows/platform-zxcron-release-jrs-regression.yaml b/.github/workflows/platform-zxcron-release-jrs-regression.yaml index a46717c63c9d..7d5a29343336 100644 --- a/.github/workflows/platform-zxcron-release-jrs-regression.yaml +++ b/.github/workflows/platform-zxcron-release-jrs-regression.yaml @@ -59,7 +59,7 @@ jobs: major="${BASH_REMATCH[1]}" minor="${BASH_REMATCH[2]}" - if [[ "${major}" -eq 0 && "${minor}" -lt 55 ]]; then + if [[ "${major}" -eq 0 && "${minor}" -lt 57 ]]; then continue fi diff --git a/.github/workflows/zxc-publish-production-image.yaml b/.github/workflows/zxc-publish-production-image.yaml index cec288c690fc..c9e24795a909 100644 --- a/.github/workflows/zxc-publish-production-image.yaml +++ b/.github/workflows/zxc-publish-production-image.yaml @@ -97,7 +97,7 @@ jobs: service_account: "swirlds-automation@hedera-registry.iam.gserviceaccount.com" - name: Setup JFrog CLI - uses: jfrog/setup-jfrog-cli@96153976e4e81b3701e9cc0a5b9597e80614af81 # v4.5.1 + uses: jfrog/setup-jfrog-cli@dff217c085c17666e8849ebdbf29c8fe5e3995e6 # v4.5.2 if: ${{ inputs.dry-run-enabled != true && inputs.registry-name == 'jfrog' && !cancelled() && !failure() }} env: JF_URL: ${{ secrets.jf-url }} @@ -140,7 +140,7 @@ jobs: uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0 - name: Setup Docker Buildx Support - uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1 + uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0 with: version: v0.16.2 driver-opts: network=host diff --git a/.github/workflows/zxc-verify-docker-build-determinism.yaml b/.github/workflows/zxc-verify-docker-build-determinism.yaml index 95d1a461de0c..ea7475d4808c 100644 --- a/.github/workflows/zxc-verify-docker-build-determinism.yaml +++ b/.github/workflows/zxc-verify-docker-build-determinism.yaml @@ -197,7 +197,7 @@ jobs: if: ${{ steps.baseline.outputs.exists == 'false' && !failure() && !cancelled() }} - name: Setup Docker Buildx Support - uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1 + uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0 if: ${{ steps.baseline.outputs.exists == 'false' && !failure() && !cancelled() }} with: version: v0.16.2 @@ -426,7 +426,7 @@ jobs: uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0 - name: Setup Docker Buildx Support - uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1 + uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0 with: version: v0.16.2 driver-opts: network=host diff --git a/hapi/hedera-protobufs/block/stream/output/state_changes.proto b/hapi/hedera-protobufs/block/stream/output/state_changes.proto index 924f8dfdbe9d..28040854aeab 100644 --- a/hapi/hedera-protobufs/block/stream/output/state_changes.proto +++ b/hapi/hedera-protobufs/block/stream/output/state_changes.proto @@ -64,12 +64,11 @@ import "state/token/token.proto"; import "state/token/token_relation.proto"; import "state/platform_state.proto"; import "timestamp.proto"; -import "auxiliary/tss/tss_encryption_key.proto"; import "auxiliary/tss/tss_message.proto"; import "auxiliary/tss/tss_vote.proto"; +import "state/tss/tss_encryption_keys.proto"; import "state/tss/tss_message_map_key.proto"; import "state/tss/tss_vote_map_key.proto"; -import "state/tss/tss_status.proto"; /** * A set of state changes. @@ -609,11 +608,6 @@ message SingletonUpdateChange { * A change to the roster state singleton. */ com.hedera.hapi.node.state.roster.RosterState roster_state_value = 13; - - /** - * A change to the tss status state singleton. - */ - com.hedera.hapi.node.state.tss.TssStatus tss_status_state_value = 14; } } @@ -892,7 +886,7 @@ message MapChangeValue { /** * The value of a map that stores tss encryption keys for each node. */ - com.hedera.hapi.services.auxiliary.tss.TssEncryptionKeyTransactionBody tss_encryption_key_value = 20; + com.hedera.hapi.node.state.tss.TssEncryptionKeys tss_encryption_keys_value = 20; /** * The value of a map that stores tss messages submitted for each share of nodes. diff --git a/hapi/hedera-protobufs/services/state/tss/tss_encryption_keys.proto b/hapi/hedera-protobufs/services/state/tss/tss_encryption_keys.proto new file mode 100644 index 000000000000..0b65d892e3e9 --- /dev/null +++ b/hapi/hedera-protobufs/services/state/tss/tss_encryption_keys.proto @@ -0,0 +1,51 @@ +/** + * # Current and next TSS encryption keys + * + * ### 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.node.state.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.node.state.tss.legacy"; +// <<>> This comment is special code for setting PBJ Compiler java package +option java_multiple_files = true; + +/** + * A message containing a node's current and next TSS encryption keys, where + * the next key (if present) will be switched to the node's current key during + * the first transaction at the beginning of a staking period.
+ */ +message TssEncryptionKeys { + + /** + * If non-empty, a node's current TSS encryption key. + */ + bytes current_encryption_key = 1; + + /** + * If non-empty, the same node's next TSS encryption key. + */ + bytes next_encryption_key = 2; +} diff --git a/hapi/hedera-protobufs/services/state/tss/tss_status.proto b/hapi/hedera-protobufs/services/state/tss/tss_status.proto deleted file mode 100644 index 362d23a5a998..000000000000 --- a/hapi/hedera-protobufs/services/state/tss/tss_status.proto +++ /dev/null @@ -1,117 +0,0 @@ -/** - * # Tss Message Map Key - * - * ### 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.node.state.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.node.state.tss.legacy"; -// <<>> This comment is special code for setting PBJ Compiler java package -option java_multiple_files = true; - -/** - * A Singleton state object that represents the status of the TSS keying process. - * - * This key SHALL be used to determine the stage of the TSS keying process. - */ -message TssStatus { - - /** - * An enum representing the status of the TSS keying process.
- *

- * This status SHALL be used to determine the state of the TSS keying process.
- * This value MUST be set when tss is enabled. - */ - TssKeyingStatus tss_keying_status = 1; - - /** - * An enum representing the key either active roster or candidate roster.
- * This value will be to key active roster if it is genesis stage - *

- * This value MUST be set. - */ - RosterToKey roster_to_key = 2; - - /** - * A hash of the ledger_id resulting from the TSS keying process.
- * If this value is empty, the TSS keying process has not yet completed. - *

- * This value COULD be empty.
- * This value MUST contain a valid hash after the TSS keying process is complete.
- */ - bytes ledger_id = 3; -} - -/** - * An enum representing the status of the TSS keying process. - * - * This status SHALL be used to determine the state of the TSS keying process. - */ -enum TssKeyingStatus { - - /** - * The TSS keying process has not yet reached the threshold for encryption - * keys. - */ - WAITING_FOR_ENCRYPTION_KEYS = 0; - - /** - * The TSS keying process has not yet reached the threshold for TSS messages. - */ - WAITING_FOR_THRESHOLD_TSS_MESSAGES = 1; - - /** - * The TSS keying process has not yet reached the threshold for TSS votes. - */ - WAITING_FOR_THRESHOLD_TSS_VOTES = 2; - - /** - * The TSS keying process has completed and the ledger id is set. - */ - KEYING_COMPLETE = 3; -} - -/** - * An enum representing the key either active roster or candidate roster. - * This value will be to key active roster if it is genesis stage. - */ -enum RosterToKey { - - /** - * Key the active roster. This is true when we are keying roster on genesis stage. - */ - ACTIVE_ROSTER = 0; - - /** - * Key the candidate roster. This is true when we are keying roster on non-genesis stage. - */ - CANDIDATE_ROSTER = 1; - - /** - * Key none of the roster. This is true when we are not keying any roster. - */ - NONE = 2; -} diff --git a/hapi/src/main/java/com/hedera/hapi/util/HapiUtils.java b/hapi/src/main/java/com/hedera/hapi/util/HapiUtils.java index efc3714b9809..d239ca16cd65 100644 --- a/hapi/src/main/java/com/hedera/hapi/util/HapiUtils.java +++ b/hapi/src/main/java/com/hedera/hapi/util/HapiUtils.java @@ -35,14 +35,12 @@ import java.util.Comparator; import java.util.EnumSet; import java.util.Set; -import java.util.regex.Pattern; /** * Utility class for working with the HAPI. We might move this to the HAPI project. */ public class HapiUtils { private static final int EVM_ADDRESS_ALIAS_LENGTH = 20; - public static final Pattern ALPHA_PRE_PATTERN = Pattern.compile("alpha[.](\\d+)"); public static final Key EMPTY_KEY_LIST = Key.newBuilder().keyList(KeyList.DEFAULT).build(); public static final long FUNDING_ACCOUNT_EXPIRY = 33197904000L; diff --git a/hedera-node/configuration/dev/genesis-network.json b/hedera-node/configuration/dev/genesis-network.json deleted file mode 100644 index 9e1c4ab07ec0..000000000000 --- a/hedera-node/configuration/dev/genesis-network.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "nodeMetadata": [{ - "rosterEntry": { - "weight": "1", - "gossipCaCertificate": "MIIDpzCCAg+gAwIBAgIJAK05TS8KZeb1MA0GCSqGSIb3DQEBDAUAMBIxEDAOBgNVBAMTB3Mtbm9kZTEwIBcNMDAwMTAxMDAwMDAwWhgPMjEwMDAxMDEwMDAwMDBaMBIxEDAOBgNVBAMTB3Mtbm9kZTEwggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQDBoP9dI3K1PRLRK7h90D9eNCfgzuHTyJi70yDEs90XJXlE6jmgf1NE2av83VAhQHLxu8Ehc/55M9Ayx9IQc0zJLSS+IrRM9QwqoG8ZvNdRgNw+je3V/8rAK/mHId+cPnnyDplCyskyi5kWCv6kTULIewFH8/KVZwhe0/hB2+N6ujWixURrxjjGLHA6b2gPoGAb/nxiVOn+L0cWcOzcyiYShxagj0FBWV7AxKx65Ynzfe7eF0gOzBUA+IM10OM5KXJejk53Xz5KpEyGe8htO/bXFlpLdm3UzrYiIhY0oKPYKECAC1s+VAZA6i+MV0nDpqDgxHRRXD8O2arauPhEI6iVT9f05AtzElrs7U95HbpQUuP1sxkaQw+bLdMOQHHMVCgMgw2g0eDdVDAMJD7wjZ+Bs6kDc/EJELb0l1uy2GEnOZMiHkK4K1r4IyZ/ed6QpyIRKfBCNyT5IIpMoVpzRYxVXgjgFdudd8iErKyvSXHThU6nu92c+vSd+FLBFHPpb6ECAwEAATANBgkqhkiG9w0BAQwFAAOCAYEAdga5NYtV48uDCd4vIsmpGWpKuUHtDVDlCvzHc2ij8DxAR6OFp+hIRNEBXkzg1KS5qP8Wba5ptmGoV4f89HemP+AL3Azde+HjpYRtffdfTdQwmMbw7xJg2lKkEo11gDo5+zPZnVbfb3FsZ+IXKji0QshQBfg+ddTkFG3TJG1ttq3ZDw94RxFQivVnkj1p+Ogel/DuBNRWQobFVe5VrmJqbuwwN8AdrPae1dMrkZatF91On5+cpVLGfk96fYUhDohDt6KKQ6DdhvFk5rhd0vsHGMQq2gAW2+Or6ZVsKkHKx8CPINpJVKAdpE0tItI+loMO02jf9oRI/8cThWP1vNAeWnr0D6m275EZf/4qem/DdJ0FJIVou3P7tsq7eSdueDnj5RmcbW/vOBtvlXpD3SqsVRn6sltZ0sk24p+6ZMzopevCZEMf/nL3OzGvSadisXb39H9DgwkNLlefju1QLgHWf0TGfeNHluDgVDhU8+/1/KUGtr2SnZ5EVO1l59FWHALj", - "gossipEndpoint": [{ - "ipAddressV4": "fwAAAQ==", - "port": 35372 - }, { - "ipAddressV4": "fwAAAQ==", - "port": 35371 - }] - } - }, { - "rosterEntry": { - "nodeId": "1", - "weight": "1", - "gossipCaCertificate": "MIIDpjCCAg6gAwIBAgIIHWg7e2Q/smQwDQYJKoZIhvcNAQEMBQAwEjEQMA4GA1UEAxMHcy1ub2RlMjAgFw0wMDAxMDEwMDAwMDBaGA8yMTAwMDEwMTAwMDAwMFowEjEQMA4GA1UEAxMHcy1ub2RlMjCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAKr5WsBepS3+y/0/yfBjzMWje7zianEz7sszrNWV3cGu2KUlR7v2+9wp/EtX1+BdcGlTTojgFs5nEBN4lM76Cp6JjFH461yN8GSkIkpe8GZnb1w4KEjZj5UYMbq+qOUI6QmwmgLeO8RHAsS6lCP1AyGFalb2ZVJ09DcYDxCRXeFj4BqvNbtD5r5DTCtpVT4ax3eb3pzNSGsjQUG9zhyp/WcsAmwmzKdMl72tk6qF8tlAWXyzwiCujWHS0Kln0C5pyEjeFNsG299toC4pgT8juxijgseTeIFRnNHmGSeSmXpAkEELlwLKR8HOnqeiS5UXNqdbxNemx/EpJSc5rTB6kzLX24dIuRsgyIIFWx73goOzmaHUolN4xmenifoMYlSNNM07WrsvmjRC5OLc/uGhdWqhZGBCH6AJB8Cmw84QLXVdHE6LiueP1oMd7g++N4X880wJkuh0ebfV3i7etUIn0jLlM50AkRucG9kwZDJ/M4LY7FT2F85R1/o2FaB/537ARQIDAQABMA0GCSqGSIb3DQEBDAUAA4IBgQB5lTkqYw0hEW+BJTFsQ8jEHfIDNRJ0kNbVuibfP+u7kzlJy15lCEi+Qw6E3d8hA1QBX3xJMxNBlrtYPrdG26hh/tOwo5Np/OfxQC5jo0Q7n7hu7aLxZRUB/q7AfdDbOun4Za6rJhT3+EsFocyARWp8bYSk3YILBMkP+2VYDRkgQidzKgKtO5yv21Y9sEgziSprc+dQb/tqn5aQZLWavFwCLwnB3t4r4qwLHkkH00Jw51uOvLeM49/t333V5Caa7wmWzMcE+KSWW0QWFRxeJrodSyjPdmDi4D8lKN5WJHSAU5L2yWIODUyWD/cvsAapTv7xXk9ja/Ssb9DpMQnM1xh0hYaESajNeL1QbGuZgPxAwrw981h7kprR2P2iMGRVGA6u4ezxmhW3s7D+yJ3+Yxs/x2J/sw65Z16mRYXRWYWHQmhgaVQjIviiAkVB6CWZo1kHl/eYaVedQzKlrTpbr3JtmwGwhYEOnrkzsC63h8/AG9gRtIAIGWGqTPWbn2pEm8M=", - "gossipEndpoint": [{ - "ipAddressV4": "fwAAAQ==", - "port": 35374 - }, { - "ipAddressV4": "fwAAAQ==", - "port": 35373 - }] - } - }, { - "rosterEntry": { - "nodeId": "2", - "weight": "1", - "gossipCaCertificate": "MIIDpzCCAg+gAwIBAgIJAJg3GRFp5bT9MA0GCSqGSIb3DQEBDAUAMBIxEDAOBgNVBAMTB3Mtbm9kZTMwIBcNMDAwMTAxMDAwMDAwWhgPMjEwMDAxMDEwMDAwMDBaMBIxEDAOBgNVBAMTB3Mtbm9kZTMwggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCl5ut2dCleDmgEneRYpAKa9Pe2qnXzgF+BEIuTfizG2OcPQi/ltv+6HxSrJXtuWNaiX/G4iP7iBzWj2ysaAYwfYj0ezTSMLRqM9hXzVgLtW0LJEF6a8vUXPsJt4GEJkUKiYCCO1MP1NLd3y/3SVJrFhwJSPqKYm2pQNg84WfPDWSkzSneOIO4Z0uWDXgs+vzSNyChWOxVieFQhLjcELtyj6narmLox+Jdo/SxUzPuktuFB3ebNgUqWPkjljgZpl00BTmbRIVHgHfDVulo2PBpXd0VplIDgdPr5zMKdTrKCuDKey8Mft72RkPKMe9LZVZ/21+rXVEh+olvvUCySsP2RkWPUJJD90c8wKo01rZsjAOXscJKQcBYlam5XXO4ZBRYzEdxuivbkPwsOoQ83swCR3alPvwfbg11Va+zXE6sRbUM9LqkYo/M3Hwg8tSIXu8oah6csputanz867dzWwyVJEPzmiXZ6ncVDQO31QlB7RndWCqKTjOQpnpblUMsrE9MCAwEAATANBgkqhkiG9w0BAQwFAAOCAYEAnUA8+kz7L+eSOm/iVvUNYF10PKO2nZtxWWL7R1vwK/2Up765PwqxKb0eSEM4bjgvZq1GuGXs9X/Y7dos42yntXvgeUY+/2JzCnw4J5tzxytZ+IKX6DR67NjDzDzVZQfptjLQrb8E7yzml0uxsqrhNPWl57Bmfe66Kg2lD11jImeeEhExlRggFukoiUWVwRNU21Q1jMUWrg2ZwfP+6fFTgRt0WR+X5zkyYPbvI6/yv7reYGjPDuZTOFhbwG8LUTQxdttDswPjnQ606kMyninL+aNelSdV/UIII7lpr/dTvgQAnrlBaGXvdy6brh3wWEwia0FZFZcKEs6M+jZ3MrFxvlTfUIdI3jRq12L10cCDi2VhORg4JmvlM+Tk6kJeSku30ZLAVo3S7GbTdvkuesOxz3UwnF7yfOA1KYOPvhv1oLxGV5z05glsn1OBKnXMdzsKFbAYYHj81bgBni2WLuIpv3oXlai2uc4y9m8LvWAQ+h/ivyog34Ai3Pvr5ZZOFgjy", - "gossipEndpoint": [{ - "ipAddressV4": "fwAAAQ==", - "port": 35376 - }, { - "ipAddressV4": "fwAAAQ==", - "port": 35375 - }] - } - }, { - "rosterEntry": { - "nodeId": "3", - "weight": "1", - "gossipCaCertificate": "MIIDpzCCAg+gAwIBAgIJAN7hww13zBZEMA0GCSqGSIb3DQEBDAUAMBIxEDAOBgNVBAMTB3Mtbm9kZTQwIBcNMDAwMTAxMDAwMDAwWhgPMjEwMDAxMDEwMDAwMDBaMBIxEDAOBgNVBAMTB3Mtbm9kZTQwggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQDK/bVyv0ZUeJZ4cIOImM+wmqtYjCw4jPAC549WQPPV1vG0lzSpgV+nRKqmWBexhLlKN3bsvrfNCUpKSq8meFyCtdppT1dhUOmEZcoNhLZzqxXb2HYYqRPv82tR+tbh+27WFsBOOqYrYTvr72ECD7qDOuw/Xob6KImaw/b/SIAPecMoYy25fkgYkJSETwd8HUpwssYH/JTLBF8eGjjTTMuu14ARQKeH8BXSs+jjV1+3IItXERS8ryUGDjqc5vC8ZW1kDVQbb91IDxRjqZbFyhuasocCqTAcZuiEgE8Wilwp2g1vbAUnHnvKNfiaEAHoEV6vF4lelaWhOnN2U5tnox/ns6PiDqIbOfs0pmXxjAK0vxc6oZM3TwdRtzo6cSb/AYfQdnmQzkra980kHN12r3f7PK2PzGBuVUPT7fLGA4S3vQDYO4rqcgTc/OLobtqLtdBusOFjZscfIfUW4GVWJUI1j+fwvHacxWLmyZwlQ5Q47UtrtjWpFru7CTn5S477lqMCAwEAATANBgkqhkiG9w0BAQwFAAOCAYEAdW6AWDhT0eOJw+0O6MYngmCgkXfFsgBC/B1plaE596hHo58FHxzCNiLFdvfRj37rxujvqsDAkADWUmOzLzLHYMXu302HzDqAMNY6FZJc32y4ZDsIQpaUOAuiNHAHwFXuPRInVpCqztfJMgw4RhOhcCTEsoIJsqoIN1t4M0pEVAv6x3nJwFKZqSNOZrQ7sOW32FjwWS3kHwRsCTtqdk5n2KxU6wr/fggV3QsSPRMYro8sUfwu93mqggtswwWqfeKlsz5WiaR9aqLnb8z1R6HLvA0bcoPWzjgn8RdP+9we4z06iZ5vdBuNpwBjrCKUELWISyAoekLGGxyS8pPqYiSBRNUoaPITSuUjcCBbJ9EFvm72QgCBesbwF71KPabTPbMPhLmf+uAi+zmeu8ZeVvT6DrX9OHSkIvIEQFry9BrqOT3ce6KBHSO1HpXIetj5Wcd3WHXtz9ulBL9ikWC8eh7/+we51ucmLvFzNKznElhT2Dp+czXUVNEUjp3u/66pyRA4", - "gossipEndpoint": [{ - "ipAddressV4": "fwAAAQ==", - "port": 35378 - }, { - "ipAddressV4": "fwAAAQ==", - "port": 35377 - }] - } - }] -} diff --git a/hedera-node/hedera-addressbook-service-impl/src/main/java/com/hedera/node/app/service/addressbook/impl/ReadableNodeStoreImpl.java b/hedera-node/hedera-addressbook-service-impl/src/main/java/com/hedera/node/app/service/addressbook/impl/ReadableNodeStoreImpl.java index 1ab44266fc0e..c0cb9b11939e 100644 --- a/hedera-node/hedera-addressbook-service-impl/src/main/java/com/hedera/node/app/service/addressbook/impl/ReadableNodeStoreImpl.java +++ b/hedera-node/hedera-addressbook-service-impl/src/main/java/com/hedera/node/app/service/addressbook/impl/ReadableNodeStoreImpl.java @@ -29,9 +29,9 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.util.ArrayList; -import java.util.Collections; import java.util.Comparator; import java.util.Iterator; +import java.util.List; /** * Provides read-only methods for interacting with the underlying data storage mechanisms for @@ -93,11 +93,11 @@ private Roster constructFromNodesState(@NonNull final ReadableKVState 1) { - Collections.swap(nodeEndpoints, 0, 1); + nodeEndpoints = List.of(nodeEndpoints.getLast(), nodeEndpoints.getFirst()); } if (!node.deleted()) { final var entry = RosterEntry.newBuilder() diff --git a/hedera-node/hedera-addressbook-service-impl/src/main/java/com/hedera/node/app/service/addressbook/impl/helpers/AddressBookHelper.java b/hedera-node/hedera-addressbook-service-impl/src/main/java/com/hedera/node/app/service/addressbook/impl/helpers/AddressBookHelper.java index 868e63d98e03..b9bd52f36e53 100644 --- a/hedera-node/hedera-addressbook-service-impl/src/main/java/com/hedera/node/app/service/addressbook/impl/helpers/AddressBookHelper.java +++ b/hedera-node/hedera-addressbook-service-impl/src/main/java/com/hedera/node/app/service/addressbook/impl/helpers/AddressBookHelper.java @@ -72,7 +72,7 @@ public void adjustPostUpgradeNodeMetadata( if (node == null) { final var newNode = Node.newBuilder() .nodeId(nodeInfo.nodeId()) - .weight(nodeInfo.stake()) + .weight(nodeInfo.weight()) .accountId(nodeInfo.accountId()) .gossipCaCertificate(nodeInfo.sigCertBytes()) .gossipEndpoint(nodeInfo.gossipEndpoints()) diff --git a/hedera-node/hedera-addressbook-service-impl/src/main/java/com/hedera/node/app/service/addressbook/impl/schemas/AddressBookTransplantSchema.java b/hedera-node/hedera-addressbook-service-impl/src/main/java/com/hedera/node/app/service/addressbook/impl/schemas/AddressBookTransplantSchema.java new file mode 100644 index 000000000000..71530c223644 --- /dev/null +++ b/hedera-node/hedera-addressbook-service-impl/src/main/java/com/hedera/node/app/service/addressbook/impl/schemas/AddressBookTransplantSchema.java @@ -0,0 +1,63 @@ +/* + * 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.addressbook.impl.schemas; + +import static com.hedera.node.app.service.addressbook.impl.schemas.V053AddressBookSchema.NODES_KEY; +import static java.util.Objects.requireNonNull; + +import com.hedera.hapi.node.state.addressbook.Node; +import com.hedera.hapi.node.state.common.EntityNumber; +import com.hedera.node.app.service.addressbook.AddressBookService; +import com.hedera.node.internal.network.Network; +import com.hedera.node.internal.network.NodeMetadata; +import com.swirlds.platform.config.AddressBookConfig; +import com.swirlds.state.lifecycle.MigrationContext; +import com.swirlds.state.lifecycle.Schema; +import com.swirlds.state.spi.WritableKVState; +import com.swirlds.state.spi.WritableStates; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * The {@link Schema#restart(MigrationContext)} implementation whereby the {@link AddressBookService} ensures that any + * node metadata overrides in the startup assets are copied into the state. + *

+ * Important: The latest {@link AddressBookService} schema should always implement this interface. + */ +public interface AddressBookTransplantSchema { + default void restart(@NonNull final MigrationContext ctx) { + requireNonNull(ctx); + if (!ctx.appConfig().getConfigData(AddressBookConfig.class).useRosterLifecycle()) { + return; + } + ctx.startupNetworks() + .overrideNetworkFor(ctx.roundNumber()) + .ifPresent(network -> setNodeMetadata(network, ctx.newStates())); + } + + /** + * Set the node metadata in the state from the provided network, for whatever nodes they are available. + * @param network the network from which to extract the node metadata + * @param writableStates the state in which to store the node metadata + */ + default void setNodeMetadata(@NonNull final Network network, @NonNull final WritableStates writableStates) { + final WritableKVState nodes = writableStates.get(NODES_KEY); + network.nodeMetadata().stream() + .filter(NodeMetadata::hasNode) + .map(NodeMetadata::nodeOrThrow) + .forEach(node -> nodes.put(new EntityNumber(node.nodeId()), node)); + } +} diff --git a/hedera-node/hedera-addressbook-service-impl/src/main/java/com/hedera/node/app/service/addressbook/impl/schemas/V053AddressBookSchema.java b/hedera-node/hedera-addressbook-service-impl/src/main/java/com/hedera/node/app/service/addressbook/impl/schemas/V053AddressBookSchema.java index 406b2a29d45a..75c5e43dc931 100644 --- a/hedera-node/hedera-addressbook-service-impl/src/main/java/com/hedera/node/app/service/addressbook/impl/schemas/V053AddressBookSchema.java +++ b/hedera-node/hedera-addressbook-service-impl/src/main/java/com/hedera/node/app/service/addressbook/impl/schemas/V053AddressBookSchema.java @@ -90,7 +90,7 @@ public void migrate(@NonNull final MigrationContext ctx) { // explicit that the override admin keys apply only at genesis final Map nodeAdminKeys = ctx.isGenesis() ? parseEd25519NodeAdminKeysFrom( - ctx.configuration().getConfigData(BootstrapConfig.class).nodeAdminKeysPath()) + ctx.appConfig().getConfigData(BootstrapConfig.class).nodeAdminKeysPath()) : emptyMap(); final var networkInfo = ctx.genesisNetworkInfo(); if (networkInfo == null) { @@ -98,7 +98,7 @@ public void migrate(@NonNull final MigrationContext ctx) { } final WritableKVState writableNodes = ctx.newStates().get(NODES_KEY); - final var bootstrapConfig = ctx.configuration().getConfigData(BootstrapConfig.class); + final var bootstrapConfig = ctx.appConfig().getConfigData(BootstrapConfig.class); log.info("Started migrating nodes from address book"); final var adminKey = getAccountAdminKey(ctx); @@ -121,7 +121,7 @@ public void migrate(@NonNull final MigrationContext ctx) { .description(formatNodeName(nodeInfo.nodeId())) .gossipEndpoint(nodeInfo.gossipEndpoints()) .gossipCaCertificate(nodeInfo.sigCertBytes()) - .weight(nodeInfo.stake()) + .weight(nodeInfo.weight()) .adminKey(nodeAdminKey); if (nodeDetailMap != null) { nodeDetail = nodeDetailMap.get(nodeInfo.nodeId()); @@ -141,7 +141,7 @@ public void migrate(@NonNull final MigrationContext ctx) { private Key getAccountAdminKey(@NonNull final MigrationContext ctx) { var adminKey = Key.DEFAULT; - final var accountConfig = ctx.configuration().getConfigData(AccountsConfig.class); + final var accountConfig = ctx.appConfig().getConfigData(AccountsConfig.class); ReadableKVState readableAccounts = null; try { @@ -163,7 +163,7 @@ private Key getAccountAdminKey(@NonNull final MigrationContext ctx) { private Map getNodeAddressMap(@NonNull final MigrationContext ctx) { Map nodeDetailMap = null; - final var fileConfig = ctx.configuration().getConfigData(FilesConfig.class); + final var fileConfig = ctx.appConfig().getConfigData(FilesConfig.class); ReadableKVState readableFiles = null; try { readableFiles = ctx.newStates().get(FILES_KEY); diff --git a/hedera-node/hedera-addressbook-service-impl/src/main/java/com/hedera/node/app/service/addressbook/impl/schemas/V057AddressBookSchema.java b/hedera-node/hedera-addressbook-service-impl/src/main/java/com/hedera/node/app/service/addressbook/impl/schemas/V057AddressBookSchema.java index af62be4eb20d..b8a949d8cd9b 100644 --- a/hedera-node/hedera-addressbook-service-impl/src/main/java/com/hedera/node/app/service/addressbook/impl/schemas/V057AddressBookSchema.java +++ b/hedera-node/hedera-addressbook-service-impl/src/main/java/com/hedera/node/app/service/addressbook/impl/schemas/V057AddressBookSchema.java @@ -16,29 +16,20 @@ package com.hedera.node.app.service.addressbook.impl.schemas; -import static com.hedera.node.app.service.addressbook.impl.schemas.V053AddressBookSchema.NODES_KEY; import static java.util.Objects.requireNonNull; import com.hedera.hapi.node.base.SemanticVersion; -import com.hedera.hapi.node.state.addressbook.Node; -import com.hedera.hapi.node.state.common.EntityNumber; import com.hedera.node.internal.network.Network; -import com.hedera.node.internal.network.NodeMetadata; +import com.swirlds.platform.config.AddressBookConfig; import com.swirlds.state.lifecycle.MigrationContext; import com.swirlds.state.lifecycle.Schema; -import com.swirlds.state.spi.WritableKVState; -import com.swirlds.state.spi.WritableStates; import edu.umd.cs.findbugs.annotations.NonNull; -import java.util.concurrent.atomic.AtomicReference; /** - * A restart-only schema that ensures address book state reflects any overrides in - * {@link Network}s returned from {@link MigrationContext#startupNetworks()} on - * disk at startup. (Note that once the network has written at least one state to - * disk after restart, all such override {@link Network}s will be archived, and - * hence are applied to at most one state after restart.) + * A genesis-only schema that ensures address book state reflects the genesis {@link Network}s returned from + * {@link MigrationContext#startupNetworks()} on disk at startup when using the roster lifecycle. */ -public class V057AddressBookSchema extends Schema { +public class V057AddressBookSchema extends Schema implements AddressBookTransplantSchema { private static final SemanticVersion VERSION = SemanticVersion.newBuilder().major(0).minor(57).build(); @@ -47,29 +38,18 @@ public V057AddressBookSchema() { } @Override - public void restart(@NonNull final MigrationContext ctx) { + public void migrate(@NonNull final MigrationContext ctx) { requireNonNull(ctx); - final AtomicReference network = new AtomicReference<>(); - if (ctx.isGenesis()) { - try { - network.set(ctx.startupNetworks().genesisNetworkOrThrow()); - } catch (Exception ignore) { - // FUTURE - fail hard here once the roster lifecycle is always enabled - return; - } - } else { - ctx.startupNetworks().overrideNetworkFor(ctx.roundNumber()).ifPresent(network::set); + if (!ctx.appConfig().getConfigData(AddressBookConfig.class).useRosterLifecycle()) { + return; } - if (network.get() != null) { - setNodeMetadata(network.get(), ctx.newStates()); + if (ctx.isGenesis()) { + setNodeMetadata(ctx.startupNetworks().genesisNetworkOrThrow(), ctx.newStates()); } } - private void setNodeMetadata(@NonNull final Network network, @NonNull final WritableStates writableStates) { - final WritableKVState nodes = writableStates.get(NODES_KEY); - network.nodeMetadata().stream() - .filter(NodeMetadata::hasNode) - .map(NodeMetadata::nodeOrThrow) - .forEach(node -> nodes.put(new EntityNumber(node.nodeId()), node)); + @Override + public void restart(@NonNull final MigrationContext ctx) { + AddressBookTransplantSchema.super.restart(ctx); } } diff --git a/hedera-node/hedera-addressbook-service-impl/src/test/java/com/hedera/node/app/service/addressbook/impl/schemas/V057AddressBookSchemaTest.java b/hedera-node/hedera-addressbook-service-impl/src/test/java/com/hedera/node/app/service/addressbook/impl/schemas/V057AddressBookSchemaTest.java index a3cd9beb8fc8..0003db3fb8ee 100644 --- a/hedera-node/hedera-addressbook-service-impl/src/test/java/com/hedera/node/app/service/addressbook/impl/schemas/V057AddressBookSchemaTest.java +++ b/hedera-node/hedera-addressbook-service-impl/src/test/java/com/hedera/node/app/service/addressbook/impl/schemas/V057AddressBookSchemaTest.java @@ -17,9 +17,11 @@ package com.hedera.node.app.service.addressbook.impl.schemas; import static com.hedera.node.app.service.addressbook.impl.schemas.V053AddressBookSchema.NODES_KEY; +import static com.hedera.node.app.service.addressbook.impl.test.handlers.AddressBookTestBase.DEFAULT_CONFIG; +import static com.hedera.node.app.service.addressbook.impl.test.handlers.AddressBookTestBase.WITH_ROSTER_LIFECYCLE; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; import com.hedera.hapi.node.state.addressbook.Node; import com.hedera.hapi.node.state.common.EntityNumber; @@ -63,25 +65,24 @@ class V057AddressBookSchemaTest { private final V057AddressBookSchema subject = new V057AddressBookSchema(); @Test - void returnsIfGenesisNodeMetadataUnavailable() { - given(ctx.isGenesis()).willReturn(true); - given(ctx.startupNetworks()).willReturn(startupNetworks); - given(startupNetworks.genesisNetworkOrThrow()).willThrow(IllegalStateException.class); + void migrationIsNoOpIfRosterLifecycleNotEnabled() { + given(ctx.appConfig()).willReturn(DEFAULT_CONFIG); subject.restart(ctx); - verifyNoInteractions(writableStates); + verifyNoMoreInteractions(ctx); } @Test void usesGenesisNodeMetadataIfPresent() { + given(ctx.appConfig()).willReturn(WITH_ROSTER_LIFECYCLE); given(ctx.startupNetworks()).willReturn(startupNetworks); given(startupNetworks.genesisNetworkOrThrow()).willReturn(NETWORK); given(ctx.newStates()).willReturn(writableStates); given(ctx.isGenesis()).willReturn(true); given(writableStates.get(NODES_KEY)).willReturn(nodes); - subject.restart(ctx); + subject.migrate(ctx); verify(nodes) .put(new EntityNumber(1L), NETWORK.nodeMetadata().getFirst().nodeOrThrow()); @@ -90,6 +91,7 @@ void usesGenesisNodeMetadataIfPresent() { @Test void usesOverrideMetadataIfPresent() { + given(ctx.appConfig()).willReturn(WITH_ROSTER_LIFECYCLE); given(ctx.startupNetworks()).willReturn(startupNetworks); given(startupNetworks.overrideNetworkFor(0L)).willReturn(Optional.of(NETWORK)); given(ctx.newStates()).willReturn(writableStates); diff --git a/hedera-node/hedera-addressbook-service-impl/src/test/java/com/hedera/node/app/service/addressbook/impl/test/AddressBookServiceImplTest.java b/hedera-node/hedera-addressbook-service-impl/src/test/java/com/hedera/node/app/service/addressbook/impl/test/AddressBookServiceImplTest.java index 55ae3ab4f68b..2c15ed934288 100644 --- a/hedera-node/hedera-addressbook-service-impl/src/test/java/com/hedera/node/app/service/addressbook/impl/test/AddressBookServiceImplTest.java +++ b/hedera-node/hedera-addressbook-service-impl/src/test/java/com/hedera/node/app/service/addressbook/impl/test/AddressBookServiceImplTest.java @@ -23,6 +23,7 @@ import static org.mockito.Mockito.verify; import com.hedera.node.app.service.addressbook.impl.AddressBookServiceImpl; +import com.hedera.node.app.service.addressbook.impl.schemas.AddressBookTransplantSchema; import com.hedera.node.app.service.addressbook.impl.schemas.V053AddressBookSchema; import com.hedera.node.app.service.addressbook.impl.schemas.V057AddressBookSchema; import com.swirlds.state.lifecycle.Schema; @@ -54,5 +55,6 @@ void registersExpectedSchema() { assertThat(schemas).hasSize(2); assertThat(schemas.getFirst()).isInstanceOf(V053AddressBookSchema.class); assertThat(schemas.getLast()).isInstanceOf(V057AddressBookSchema.class); + assertThat(schemas.getLast()).isInstanceOf(AddressBookTransplantSchema.class); } } diff --git a/hedera-node/hedera-addressbook-service-impl/src/test/java/com/hedera/node/app/service/addressbook/impl/test/handlers/AddressBookTestBase.java b/hedera-node/hedera-addressbook-service-impl/src/test/java/com/hedera/node/app/service/addressbook/impl/test/handlers/AddressBookTestBase.java index 8955ca71b4f3..484edd4983db 100644 --- a/hedera-node/hedera-addressbook-service-impl/src/test/java/com/hedera/node/app/service/addressbook/impl/test/handlers/AddressBookTestBase.java +++ b/hedera-node/hedera-addressbook-service-impl/src/test/java/com/hedera/node/app/service/addressbook/impl/test/handlers/AddressBookTestBase.java @@ -96,6 +96,9 @@ public class AddressBookTestBase { A_COMPLEX_KEY))) .build(); public static final Configuration DEFAULT_CONFIG = HederaTestConfigBuilder.createConfig(); + public static final Configuration WITH_ROSTER_LIFECYCLE = HederaTestConfigBuilder.create() + .withValue("addressBook.useRosterLifecycle", true) + .getOrCreateConfig(); protected final Key key = A_COMPLEX_KEY; protected final Key anotherKey = B_COMPLEX_KEY; 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 f56d46b5a78f..7cb776e649ad 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 @@ -305,7 +305,7 @@ private void setupMigrationContext() { final var config = HederaTestConfigBuilder.create() .withValue("bootstrap.genesisPublicKey", defaultAdminKeyBytes) .getOrCreateConfig(); - given(migrationContext.configuration()).willReturn(config); + given(migrationContext.appConfig()).willReturn(config); } private void setupMigrationContext2() { @@ -332,7 +332,7 @@ private void setupMigrationContext2() { adminKeysLoc.toAbsolutePath().toString()) .withValue("accounts.addressBookAdmin", "55") .getOrCreateConfig(); - given(migrationContext.configuration()).willReturn(config); + given(migrationContext.appConfig()).willReturn(config); } private void setupMigrationContext3() { @@ -367,7 +367,7 @@ private void setupMigrationContext3() { .withValue("accounts.addressBookAdmin", "55") .withValue("files.nodeDetails", "102") .getOrCreateConfig(); - given(migrationContext.configuration()).willReturn(config); + given(migrationContext.appConfig()).willReturn(config); } private void setupMigrationContext4() { @@ -388,7 +388,7 @@ private void setupMigrationContext4() { .withValue("accounts.addressBookAdmin", "55") .withValue("files.nodeDetails", "102") .getOrCreateConfig(); - given(migrationContext.configuration()).willReturn(config); + given(migrationContext.appConfig()).willReturn(config); } private String nodeAdminKeysJson() { 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 9838e2cc658c..0eda16f58924 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 @@ -20,7 +20,6 @@ import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.ServiceEndpoint; -import com.hedera.hapi.node.state.roster.Roster; import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.common.platform.NodeId; import com.swirlds.state.State; @@ -90,15 +89,10 @@ public void updateFrom(final State state) { throw new UnsupportedOperationException("Not implemented"); } - @Override - public Roster roster() { - return Roster.DEFAULT; - } - private static NodeInfo fakeInfoWith( final long nodeId, @NonNull final AccountID nodeAccountId, - long stake, + long weight, List gossipEndpoints, @Nullable Bytes sigCertBytes) { return new NodeInfo() { @@ -113,8 +107,8 @@ public AccountID accountId() { } @Override - public long stake() { - return stake; + public long weight() { + return weight; } @Override 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 741bfbd2f1ce..fc47299d054d 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 @@ -36,7 +36,6 @@ import static com.hedera.node.app.util.HederaAsciiArt.HEDERA; import static com.hedera.node.config.types.StreamMode.BLOCKS; import static com.hedera.node.config.types.StreamMode.RECORDS; -import static com.swirlds.platform.roster.RosterRetriever.buildRoster; 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; @@ -115,6 +114,7 @@ import com.hedera.node.config.data.NetworkAdminConfig; import com.hedera.node.config.data.VersionConfig; import com.hedera.node.config.types.StreamMode; +import com.hedera.node.internal.network.Network; import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.common.constructable.ClassConstructorPair; import com.swirlds.common.constructable.ConstructableRegistry; @@ -133,12 +133,10 @@ import com.swirlds.platform.listeners.ReconnectCompleteListener; import com.swirlds.platform.listeners.ReconnectCompleteNotification; import com.swirlds.platform.listeners.StateWriteToDiskCompleteListener; -import com.swirlds.platform.roster.RosterUtils; import com.swirlds.platform.state.MerkleRoot; import com.swirlds.platform.state.PlatformMerkleStateRoot; import com.swirlds.platform.state.service.PlatformStateService; import com.swirlds.platform.state.service.ReadablePlatformStateStore; -import com.swirlds.platform.state.service.ReadableRosterStoreImpl; import com.swirlds.platform.system.InitTrigger; import com.swirlds.platform.system.Platform; import com.swirlds.platform.system.Round; @@ -285,11 +283,6 @@ public final class Hedera implements SwirldMain, PlatformStatusChangeListener, A */ private final StartupNetworksFactory startupNetworksFactory; - /** - * The id of this node. - */ - private final NodeId selfNodeId; - /** * The Hashgraph Platform. This is set during state initialization. */ @@ -310,6 +303,15 @@ public final class Hedera implements SwirldMain, PlatformStatusChangeListener, A */ private HederaInjectionComponent daggerApp; + /** + * When applying and migrating schemas to a target state, it is set here to support + * giving the {@link RosterService} schemas access to a {@link ReadablePlatformStateStore} + * before the roster lifecycle is adopted. + */ + @Nullable + @Deprecated + private State initState; + /** * The metrics object being used for reporting. */ @@ -360,8 +362,7 @@ public interface TssBaseServiceFactory { @FunctionalInterface public interface StartupNetworksFactory { @NonNull - StartupNetworks apply( - long selfNodeId, @NonNull ConfigProvider configProvider, @NonNull TssBaseService tssBaseService); + StartupNetworks apply(@NonNull ConfigProvider configProvider, @NonNull TssBaseService tssBaseService); } /*================================================================================================================== @@ -382,7 +383,6 @@ StartupNetworks apply( * @param migrator the migrator to use with the services * @param tssBaseServiceFactory the factory for the TSS base service * @param startupNetworksFactory the factory for the startup networks - * @param selfNodeId the node ID of this node */ public Hedera( @NonNull final ConstructableRegistry constructableRegistry, @@ -390,11 +390,9 @@ public Hedera( @NonNull final ServiceMigrator migrator, @NonNull final InstantSource instantSource, @NonNull final TssBaseServiceFactory tssBaseServiceFactory, - @NonNull final StartupNetworksFactory startupNetworksFactory, - @NonNull final NodeId selfNodeId) { + @NonNull final StartupNetworksFactory startupNetworksFactory) { requireNonNull(registryFactory); requireNonNull(constructableRegistry); - this.selfNodeId = requireNonNull(selfNodeId); this.serviceMigrator = requireNonNull(migrator); this.startupNetworksFactory = requireNonNull(startupNetworksFactory); logger.info( @@ -460,7 +458,10 @@ public Hedera( // FUTURE: a lambda that tests if a ReadableTssStore // constructed from the migration state returns a // RosterKeys with the ledger id for the given roster - new RosterService(roster -> true), + new RosterService( + roster -> true, + () -> new ReadablePlatformStateStore( + requireNonNull(initState).getReadableStates(PlatformStateService.NAME))), PLATFORM_STATE_SERVICE) .forEach(servicesRegistry::register); try { @@ -539,10 +540,11 @@ public void initializeStatesApi( @NonNull final State state, @NonNull final Metrics metrics, @NonNull final InitTrigger trigger, - @Nullable final AddressBook genesisAddressBook, - @NonNull final Configuration platformConfiguration) { + @Nullable final Network genesisNetwork, + @NonNull final Configuration platformConfig, + @Deprecated @Nullable final AddressBook diskAddressBook) { requireNonNull(state); - requireNonNull(platformConfiguration); + requireNonNull(platformConfig); this.metrics = requireNonNull(metrics); this.configProvider = new ConfigProviderImpl(trigger == GENESIS, metrics); final var deserializedVersion = serviceMigrator.creationVersionOf(state); @@ -568,7 +570,7 @@ public void initializeStatesApi( throw new IllegalStateException("Cannot downgrade from " + savedStateVersion + " to " + version); } try { - migrateSchemas(state, savedStateVersion, trigger, metrics, genesisAddressBook, platformConfiguration); + migrateSchemas(state, savedStateVersion, trigger, metrics, genesisNetwork, platformConfig, diskAddressBook); logConfiguration(); } catch (final Throwable t) { logger.fatal("Critical failure during schema migration", t); @@ -598,11 +600,7 @@ public void onStateInitialized( this.platform = requireNonNull(platform); if (state.getReadableStates(PlatformStateService.NAME).isEmpty()) { initializeStatesApi( - state, - metrics, - trigger, - RosterUtils.buildAddressBook(platform.getRoster()), - platform.getContext().getConfiguration()); + state, metrics, trigger, null, platform.getContext().getConfiguration(), null); } // With the States API grounded in the working state, we can create the object graph from it initializeDagger(state, trigger); @@ -618,19 +616,21 @@ public void onStateInitialized( *

If the {@code deserializedVersion} is {@code null}, then this is the first time the node has been started, * and thus all schemas will be executed. * - * @param state current state - * @param deserializedVersion version deserialized - * @param trigger trigger that is calling migration - * @param genesisAddressBook the genesis address book, if applicable - * @param platformConfiguration platform configuration + * @param state current state + * @param deserializedVersion version deserialized + * @param trigger trigger that is calling migration + * @param genesisNetwork the genesis address book, if applicable + * @param platformConfig platform configuration + * @param diskAddressBook before enabling the roster lifecycle, the address book from disk */ private void migrateSchemas( @NonNull final State state, @Nullable final ServicesSoftwareVersion deserializedVersion, @NonNull final InitTrigger trigger, @NonNull final Metrics metrics, - @Nullable final AddressBook genesisAddressBook, - @NonNull final Configuration platformConfiguration) { + @Nullable final Network genesisNetwork, + @NonNull final Configuration platformConfig, + @Deprecated @Nullable final AddressBook diskAddressBook) { final var previousVersion = deserializedVersion == null ? null : deserializedVersion.getPbjSemanticVersion(); final var isUpgrade = version.compareTo(deserializedVersion) > 0; logger.info( @@ -647,14 +647,17 @@ private void migrateSchemas( if (trigger == GENESIS) { final var config = configProvider.getConfiguration(); final var ledgerConfig = config.getConfigData(LedgerConfig.class); - final var genesisRoster = buildRoster(requireNonNull(genesisAddressBook)); - genesisNetworkInfo = new GenesisNetworkInfo(genesisRoster, ledgerConfig.id()); + genesisNetworkInfo = new GenesisNetworkInfo(requireNonNull(genesisNetwork), ledgerConfig.id()); } blockStreamService.resetMigratedLastBlockHash(); - startupNetworks = startupNetworksFactory.apply(selfNodeId.id(), configProvider, tssBaseService); - PLATFORM_STATE_SERVICE.setAppVersionFn(() -> version); - PLATFORM_STATE_SERVICE.setActiveRosterFn( - () -> new ReadableRosterStoreImpl(state.getReadableStates(RosterService.NAME)).getActiveRoster()); + startupNetworks = startupNetworksFactory.apply(configProvider, tssBaseService); + PLATFORM_STATE_SERVICE.setAppVersionFn(ServicesSoftwareVersion::from); + // If the client code did not provide a disk address book, we are reconnecting; and + // PlatformState schemas must not try to update the current address book anyway + if (diskAddressBook != null) { + PLATFORM_STATE_SERVICE.setDiskAddressBook(diskAddressBook); + } + this.initState = state; final var migrationChanges = serviceMigrator.doMigrations( state, servicesRegistry, @@ -663,11 +666,12 @@ private void migrateSchemas( // (FUTURE) In principle, the FileService could change the active configuration during a // migration, implying we should pass a config provider; but we don't need this yet configProvider.getConfiguration(), - platformConfiguration, + platformConfig, genesisNetworkInfo, metrics, startupNetworks); - PLATFORM_STATE_SERVICE.clearActiveRosterFn(); + this.initState = null; + PLATFORM_STATE_SERVICE.clearDiskAddressBook(); migrationStateChanges = new ArrayList<>(migrationChanges); kvStateChangeListener.reset(); boundaryStateChangeListener.reset(); @@ -1005,8 +1009,7 @@ private void initializeDagger(@NonNull final State state, @NonNull final InitTri final var activeRoster = tssBaseService.chooseRosterForNetwork( state, trigger, serviceMigrator, version, configProvider.getConfiguration(), platform.getRoster()); - final var networkInfo = - new StateNetworkInfo(state, activeRoster, platform.getSelfId().id(), configProvider); + final var networkInfo = new StateNetworkInfo(platform.getSelfId().id(), state, activeRoster, configProvider); // Fully qualified so as to not confuse javadoc daggerApp = com.hedera.node.app.DaggerHederaInjectionComponent.builder() .configProviderImpl(configProvider) 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 3bfa8a205153..870dde6c4779 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 @@ -20,6 +20,7 @@ import static com.swirlds.common.io.utility.FileUtils.rethrowIO; import static com.swirlds.common.threading.manager.AdHocThreadManager.getStaticThreadManager; import static com.swirlds.logging.legacy.LogMarker.EXCEPTION; +import static com.swirlds.logging.legacy.LogMarker.STARTUP; import static com.swirlds.platform.builder.PlatformBuildConstants.DEFAULT_CONFIG_FILE_NAME; import static com.swirlds.platform.builder.PlatformBuildConstants.DEFAULT_SETTINGS_FILE_NAME; import static com.swirlds.platform.builder.PlatformBuildConstants.LOG4J_FILE_NAME; @@ -27,17 +28,20 @@ import static com.swirlds.platform.builder.internal.StaticPlatformBuilder.setupGlobalMetrics; import static com.swirlds.platform.config.internal.PlatformConfigUtils.checkConfiguration; import static com.swirlds.platform.crypto.CryptoStatic.initNodeSecurity; -import static com.swirlds.platform.state.signed.StartupStateUtils.getInitialState; +import static com.swirlds.platform.roster.RosterUtils.buildRosterHistory; +import static com.swirlds.platform.state.signed.StartupStateUtils.copyInitialSignedState; import static com.swirlds.platform.system.SystemExitCode.CONFIGURATION_ERROR; import static com.swirlds.platform.system.SystemExitCode.NODE_ADDRESS_MISMATCH; import static com.swirlds.platform.system.SystemExitUtils.exitSystem; -import static com.swirlds.platform.system.address.AddressBookUtils.initializeAddressBook; import static com.swirlds.platform.util.BootstrapUtils.checkNodesToRun; +import static com.swirlds.platform.util.BootstrapUtils.detectSoftwareUpgrade; import static com.swirlds.platform.util.BootstrapUtils.getNodesToRun; import static java.util.Objects.requireNonNull; import com.google.common.annotations.VisibleForTesting; import com.hedera.node.app.info.DiskStartupNetworks; +import com.hedera.node.app.service.addressbook.AddressBookService; +import com.hedera.node.app.service.addressbook.impl.ReadableNodeStoreImpl; import com.hedera.node.app.services.OrderedServiceMigrator; import com.hedera.node.app.services.ServicesRegistryImpl; import com.hedera.node.app.store.ReadableStoreFactory; @@ -59,6 +63,7 @@ import com.swirlds.config.api.ConfigurationBuilder; import com.swirlds.config.extensions.sources.SystemEnvironmentConfigSource; import com.swirlds.config.extensions.sources.SystemPropertiesConfigSource; +import com.swirlds.logging.legacy.payload.SavedStateLoadedPayload; import com.swirlds.metrics.api.Metrics; import com.swirlds.platform.Browser; import com.swirlds.platform.CommandLineArgs; @@ -68,18 +73,22 @@ import com.swirlds.platform.config.legacy.ConfigurationException; import com.swirlds.platform.config.legacy.LegacyConfigProperties; import com.swirlds.platform.config.legacy.LegacyConfigPropertiesLoader; +import com.swirlds.platform.crypto.CryptoStatic; import com.swirlds.platform.roster.RosterHistory; import com.swirlds.platform.roster.RosterUtils; import com.swirlds.platform.state.MerkleRoot; +import com.swirlds.platform.state.PlatformMerkleStateRoot; +import com.swirlds.platform.state.address.AddressBookInitializer; import com.swirlds.platform.state.service.ReadableRosterStore; +import com.swirlds.platform.state.signed.HashedReservedSignedState; import com.swirlds.platform.state.signed.SignedState; +import com.swirlds.platform.state.signed.StartupStateUtils; import com.swirlds.platform.system.InitTrigger; import com.swirlds.platform.system.Platform; import com.swirlds.platform.system.SoftwareVersion; import com.swirlds.platform.system.SwirldMain; import com.swirlds.platform.system.SwirldState; import com.swirlds.platform.system.address.AddressBook; -import com.swirlds.platform.system.address.AddressBookUtils; import com.swirlds.platform.util.BootstrapUtils; import com.swirlds.state.State; import com.swirlds.state.merkle.MerkleStateRoot; @@ -89,6 +98,7 @@ import java.util.Set; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -98,7 +108,6 @@ *

This class simply delegates to {@link Hedera}. */ public class ServicesMain implements SwirldMain { - private static final Logger logger = LogManager.getLogger(ServicesMain.class); /** @@ -162,8 +171,8 @@ public void run() { * {@link Hedera#newMerkleStateRoot()} method. *

  • Determine this node's self id by searching the config.txt * in the working directory for any address book entries with IP addresses - * local to this machine; if there is there is more than one such entry, - * fail unless the command line args include a {@literal -local N} arg.
  • + * local to this machine; if there is more than one such entry, fail unless + * the command line args include a {@literal -local N} arg. *
  • Build a {@link Platform} instance from Services application metadata * and the working directory settings.txt, providing the same * {@link Hedera#newMerkleStateRoot()} method reference as the genesis state @@ -179,173 +188,166 @@ public void run() { *

    Please see the startup-phase-lifecycle.png in this directory to visualize * the sequence of events in the startup phase and the centrality of the {@link Hedera} * singleton. + *

    + * IMPORTANT: A surface-level reading of this method will undersell the centrality + * of the Hedera instance. It is actually omnipresent throughout both the startup and + * runtime phases of the application. Let's see why. When we build the platform, the + * builder will either: + *

      + *
    1. Create a genesis state; or,
    2. + *
    3. Deserialize a saved state.
    4. + *
    + * In both cases the state object will be created by the {@link Hedera#newMerkleStateRoot()} + * method reference bound to our Hedera instance. Because, + *
      + *
    1. We provided this method as the genesis state factory right above; and,
    2. + *
    3. Our Hedera instance's constructor registered its {@link Hedera#newMerkleStateRoot()} + * method with the {@link ConstructableRegistry} as the factory for the Services state root + * class id.
    4. + *
    + * Now, note that {@link Hedera#newMerkleStateRoot()} returns {@link PlatformMerkleStateRoot} + * instances that delegate their lifecycle methods to an injected instance of + * {@link com.swirlds.platform.state.MerkleStateLifecycles}---and the implementation of that + * injected by {@link Hedera#newMerkleStateRoot()} delegates these calls back to the Hedera + * instance itself. + *

    + * Thus, the Hedera instance centralizes nearly all the setup and runtime logic for the + * application. It implements this logic by instantiating a {@link javax.inject.Singleton} + * component whose object graph roots include the Ingest, PreHandle, Handle, and Query + * workflows; as well as other infrastructure components that need to be initialized or + * accessed at specific points in the Swirlds application lifecycle. * * @param args optionally, what node id to run; required if the address book is ambiguous */ public static void main(final String... args) throws Exception { + // --- Configure platform infrastructure and context from the command line and environment --- BootstrapUtils.setupConstructableRegistry(); - // Determine which node to run locally - // Load config.txt address book file and parse address book - final AddressBook diskAddressBook = loadAddressBook(DEFAULT_CONFIG_FILE_NAME); - // parse command line arguments - final CommandLineArgs commandLineArgs = CommandLineArgs.parse(args); - - // Only allow 1 node to be specified by the command line arguments. + final var diskAddressBook = loadAddressBook(DEFAULT_CONFIG_FILE_NAME); + final var commandLineArgs = CommandLineArgs.parse(args); if (commandLineArgs.localNodesToStart().size() > 1) { logger.error( EXCEPTION.getMarker(), "Multiple nodes were supplied via the command line. Only one node can be started per java process."); exitSystem(NODE_ADDRESS_MISMATCH); } - - // get the list of configured nodes from the address book - // for each node in the address book, check if it has a local IP (local to this computer) - // additionally if a command line arg is supplied then limit matching nodes to that node id final List nodesToRun = getNodesToRun(diskAddressBook, commandLineArgs.localNodesToStart()); - // hard exit if no nodes are configured to run checkNodesToRun(nodesToRun); - - final NodeId selfId = ensureSingleNode(nodesToRun, commandLineArgs.localNodesToStart()); - - final var configuration = buildConfiguration(); - - // Register with the ConstructableRegistry classes which need configuration. - BootstrapUtils.setupConstructableRegistryWithConfiguration(configuration); - - final var keysAndCerts = - initNodeSecurity(diskAddressBook, configuration).get(selfId); - - setupGlobalMetrics(configuration); + final var selfId = ensureSingleNode(nodesToRun, commandLineArgs.localNodesToStart()); + final var platformConfig = buildPlatformConfig(); + BootstrapUtils.setupConstructableRegistryWithConfiguration(platformConfig); + final var networkKeysAndCerts = initNodeSecurity(diskAddressBook, platformConfig); + final var keysAndCerts = networkKeysAndCerts.get(selfId); + setupGlobalMetrics(platformConfig); metrics = getMetricsProvider().createPlatformMetrics(selfId); - - hedera = newHedera(selfId); - final SoftwareVersion version = hedera.getSoftwareVersion(); - logger.info("Starting node {} with version {}", selfId, version); - final var time = Time.getCurrent(); - final var fileSystemManager = FileSystemManager.create(configuration); + final var fileSystemManager = FileSystemManager.create(platformConfig); final var recycleBin = - RecycleBin.create(metrics, configuration, getStaticThreadManager(), time, fileSystemManager, selfId); - + RecycleBin.create(metrics, platformConfig, getStaticThreadManager(), time, fileSystemManager, selfId); final var cryptography = CryptographyFactory.create(); CryptographyHolder.set(cryptography); - // the AddressBook is not changed after this point, so we calculate the hash now cryptography.digestSync(diskAddressBook); - - // Initialize the Merkle cryptography - final var merkleCryptography = MerkleCryptographyFactory.create(configuration, cryptography); + final var merkleCryptography = MerkleCryptographyFactory.create(platformConfig, cryptography); MerkleCryptoFactory.set(merkleCryptography); + final var platformContext = PlatformContext.create( + platformConfig, + Time.getCurrent(), + metrics, + cryptography, + FileSystemManager.create(platformConfig), + recycleBin, + merkleCryptography); - // Create initial state for the platform + // --- Construct the Hedera instance and use it to initialize the starting state --- + hedera = newHedera(selfId, metrics); + final var version = hedera.getSoftwareVersion(); + logger.info("Starting node {} with version {}", selfId, version); final var isGenesis = new AtomicBoolean(false); // We want to be able to see the schema migration logs, so init logging here initLogging(); - final var reservedState = getInitialState( - configuration, + final var reservedState = loadInitialState( + platformConfig, recycleBin, version, () -> { isGenesis.set(true); - final var genesisState = hedera.newMerkleStateRoot(); + final var genesisState = (PlatformMerkleStateRoot) hedera.newMerkleStateRoot(); + final var genesisNetwork = DiskStartupNetworks.fromLegacyAddressBook(diskAddressBook); hedera.initializeStatesApi( - (MerkleStateRoot) genesisState, + genesisState, metrics, InitTrigger.GENESIS, - diskAddressBook, - configuration); + genesisNetwork, + platformConfig, + diskAddressBook); return genesisState; }, Hedera.APP_NAME, Hedera.SWIRLD_NAME, - selfId, - diskAddressBook); + selfId); final var initialState = reservedState.state(); if (!isGenesis.get()) { hedera.initializeStatesApi( - (MerkleStateRoot) initialState.get().getState().getSwirldState(), + (PlatformMerkleStateRoot) initialState.get().getState().getSwirldState(), metrics, InitTrigger.RESTART, null, - configuration); + platformConfig, + diskAddressBook); } + hedera.setInitialStateHash(reservedState.hash()); - // Create the platform context - final var platformContext = PlatformContext.create( - configuration, - Time.getCurrent(), - metrics, - cryptography, - FileSystemManager.create(configuration), - recycleBin, - merkleCryptography); - - final var stateHash = reservedState.hash(); - - // Initialize the address book and set on platform builder - final var addressBook = initializeAddressBook(selfId, version, initialState, diskAddressBook, platformContext); - + // --- Now build the platform and start it --- + final var stateRoot = (PlatformMerkleStateRoot) initialState.get().getState(); + // Roster history naturally derives from RosterService state if the roster + // lifecycle is enabled; but until then, is translated from the legacy + // previous and current AddressBook fields in the PlatformState final RosterHistory rosterHistory; - final boolean shouldUseRosterLifecycle = - configuration.getConfigData(AddressBookConfig.class).useRosterLifecycle(); - if (shouldUseRosterLifecycle) { - final SignedState loadedSignedState = initialState.get(); - final var state = ((MerkleStateRoot) loadedSignedState.getState()); - final var rosterStore = new ReadableStoreFactory(state).getStore(ReadableRosterStore.class); + if (platformConfig.getConfigData(AddressBookConfig.class).useRosterLifecycle()) { + final var rosterStore = new ReadableStoreFactory(stateRoot).getStore(ReadableRosterStore.class); rosterHistory = RosterUtils.createRosterHistory(rosterStore); } else { - rosterHistory = - RosterUtils.buildRosterHistory((State) initialState.get().getState()); + // This constructor both does extensive validation and has the side effect of + // moving unused config.txt files to an archive directory; so keep calling it + // here until we enable the roster lifecycle + new AddressBookInitializer( + selfId, + version, + detectSoftwareUpgrade(version, initialState.get()), + initialState.get(), + diskAddressBook.copy(), + platformContext); + rosterHistory = buildRosterHistory((State) initialState.get().getState()); } - - // Follow the Inversion of Control pattern by injecting all needed dependencies into the PlatformBuilder. final var platformBuilder = PlatformBuilder.create( Hedera.APP_NAME, Hedera.SWIRLD_NAME, version, initialState, selfId, - AddressBookUtils.formatConsensusEventStreamName(addressBook, selfId), - // 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) + canonicalEventStreamLoc(selfId.id(), stateRoot), rosterHistory) .withPlatformContext(platformContext) - .withConfiguration(configuration) + .withConfiguration(platformConfig) .withKeysAndCerts(keysAndCerts); - - hedera.setInitialStateHash(stateHash); - // IMPORTANT: A surface-level reading of this method will undersell the centrality - // of the Hedera instance. It is actually omnipresent throughout both the startup - // and runtime phases of the application. - // - // Let's see why. When we build the platform, the builder will either: - // (1) Create a genesis state; or, - // (2) Deserialize a saved state. - // In both cases the state object will be created by the hedera::newState method - // reference bound to our Hedera instance. Because, - // (1) We provided this method as the genesis state factory right above; and, - // (2) Our Hedera instance's constructor registered its newState() method with the - // ConstructableRegistry as the factory for the Services Merkle tree class id. - // - // Now, note that hedera::newState returns MerkleStateRoot instances that delegate - // their lifecycle methods to an injected instance of MerkleStateLifecycles---and - // hedera::newState injects an instance of MerkleStateLifecyclesImpl which primarily - // delegates these calls back to the Hedera instance itself. - // - // Thus, the Hedera instance centralizes nearly all the setup and runtime logic for the - // application. It implements this logic by instantiating a Dagger2 @Singleton component - // whose object graph roots include the Ingest, PreHandle, Handle, and Query workflows; - // as well as other infrastructure components that need to be initialized or accessed - // at specific points in the Swirlds application lifecycle. - final Platform platform = platformBuilder.build(); + final var platform = platformBuilder.build(); hedera.init(platform, selfId); platform.start(); hedera.run(); } + /** + * Returns the event stream name for the given node id. + * + * @param nodeId the node id + * @param root the platform merkle state root + * @return the event stream name + */ + private static String canonicalEventStreamLoc(final long nodeId, @NonNull final PlatformMerkleStateRoot root) { + final var nodeStore = new ReadableNodeStoreImpl(root.getReadableStates(AddressBookService.NAME)); + final var accountId = requireNonNull(nodeStore.get(nodeId)).accountIdOrThrow(); + return accountId.shardNum() + "." + accountId.realmNum() + "." + accountId.accountNumOrThrow(); + } + private static void initLogging() { final var log4jPath = getAbsolutePath(LOG4J_FILE_NAME); try { @@ -359,12 +361,37 @@ private static void initLogging() { } /** - * Build the configuration for this node. + * Creates a canonical {@link Hedera} instance for the given node id and metrics. + * + * @param selfNodeId the node id + * @param metrics the metrics + * @return the {@link Hedera} instance + */ + public static Hedera newHedera(@NonNull final NodeId selfNodeId, @NonNull final Metrics metrics) { + requireNonNull(selfNodeId); + requireNonNull(metrics); + return new Hedera( + ConstructableRegistry.getInstance(), + ServicesRegistryImpl::new, + new OrderedServiceMigrator(), + InstantSource.system(), + appContext -> new TssBaseServiceImpl( + appContext, + ForkJoinPool.commonPool(), + ForkJoinPool.commonPool(), + new TssLibraryImpl(appContext), + ForkJoinPool.commonPool(), + metrics), + DiskStartupNetworks::new); + } + + /** + * Builds the platform configuration for this node. * * @return the configuration */ @NonNull - private static Configuration buildConfiguration() { + public static Configuration buildPlatformConfig() { final ConfigurationBuilder configurationBuilder = ConfigurationBuilder.create() .withSource(SystemEnvironmentConfigSource.getInstance()) .withSource(SystemPropertiesConfigSource.getInstance()); @@ -436,25 +463,49 @@ private static AddressBook loadAddressBook(@NonNull final String addressBookPath } } - private static @NonNull Hedera hederaOrThrow() { - return requireNonNull(hedera); + /** + * Get the initial state to be used by this node. May return a state loaded from disk, or may return a genesis state + * if no valid state is found on disk. + * + * @param configuration the configuration for this node + * @param softwareVersion the software version of the app + * @param stateRootSupplier a supplier that can build a genesis state + * @param mainClassName the name of the app's SwirldMain class + * @param swirldName the name of this swirld + * @param selfId the node id of this node + * @return the initial state to be used by this node + */ + @NonNull + private static HashedReservedSignedState loadInitialState( + @NonNull final Configuration configuration, + @NonNull final RecycleBin recycleBin, + @NonNull final SoftwareVersion softwareVersion, + @NonNull final Supplier stateRootSupplier, + @NonNull final String mainClassName, + @NonNull final String swirldName, + @NonNull final NodeId selfId) { + final var loadedState = StartupStateUtils.loadStateFile( + configuration, recycleBin, selfId, mainClassName, swirldName, softwareVersion); + try (loadedState) { + if (loadedState.isNotNull()) { + logger.info( + STARTUP.getMarker(), + new SavedStateLoadedPayload( + loadedState.get().getRound(), loadedState.get().getConsensusTimestamp())); + return copyInitialSignedState(configuration, loadedState.get()); + } + } + final var stateRoot = stateRootSupplier.get(); + final var signedState = new SignedState( + configuration, CryptoStatic::verifySignature, stateRoot, "genesis state", false, false, false); + final var reservedSignedState = signedState.reserve("initial reservation on genesis state"); + try (reservedSignedState) { + return copyInitialSignedState(configuration, reservedSignedState.get()); + } } - private static Hedera newHedera(@NonNull final NodeId selfNodeId) { - return new Hedera( - ConstructableRegistry.getInstance(), - ServicesRegistryImpl::new, - new OrderedServiceMigrator(), - InstantSource.system(), - appContext -> new TssBaseServiceImpl( - appContext, - ForkJoinPool.commonPool(), - ForkJoinPool.commonPool(), - new TssLibraryImpl(appContext), - ForkJoinPool.commonPool(), - metrics), - DiskStartupNetworks::new, - selfNodeId); + private static @NonNull Hedera hederaOrThrow() { + return requireNonNull(hedera); } @VisibleForTesting diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/StartupAssets.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/StartupAssets.java deleted file mode 100644 index 9c2c0033eeef..000000000000 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/StartupAssets.java +++ /dev/null @@ -1,52 +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; - -import com.hedera.node.internal.network.Network; -import edu.umd.cs.findbugs.annotations.NonNull; -import java.nio.file.Path; -import java.util.Optional; - -public interface StartupAssets { - interface Factory { - StartupAssets fromInitialConditions(@NonNull Path workingDir); - } - - /** - * Called by a node that finds itself with an empty RosterService - * state, and is thus at the migration boundary for adoption of the - * proposed roster; implementations must either throw unsupported or - * return an aggregation of the information in legacy config.txt and - * public.pfx files. - */ - Network migrationNetworkOrThrow(); - - /** - * Called by a node that finds itself with a completely empty state - * and no genesis-config.txt file. - */ - Network genesisNetworkOrThrow(); - - /** - * Returns a Network description if there is an override-config.txt - * on disk that has not already been used in an earlier round than the - * given number. - */ - Optional overrideNetwork(long roundNumber); - - void archiveInitialConditions(); -} 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 051f6f7e8d3b..f19801262018 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 @@ -358,7 +358,12 @@ public void endRound(@NonNull final State state, final long roundNum) { case ONLY_FREEZE_BLOCK -> roundNum == freezeRoundNumber; }; if (exportNetworkToDisk) { - DiskStartupNetworks.writeNetworkInfo(state, Paths.get(diskNetworkExportFile)); + final var exportPath = Paths.get(diskNetworkExportFile); + log.info( + "Writing network info to disk @ {} (REASON = {})", + exportPath.toAbsolutePath(), + diskNetworkExport); + DiskStartupNetworks.writeNetworkInfo(state, exportPath); } } } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/BoundaryStateChangeListener.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/BoundaryStateChangeListener.java index f0691852106e..4da8ca14d14a 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/BoundaryStateChangeListener.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/BoundaryStateChangeListener.java @@ -39,7 +39,6 @@ import com.hedera.hapi.node.state.roster.RosterState; import com.hedera.hapi.node.state.throttles.ThrottleUsageSnapshots; import com.hedera.hapi.node.state.token.NetworkStakingRewards; -import com.hedera.hapi.node.state.tss.TssStatus; import com.hedera.hapi.node.transaction.ExchangeRateSet; import com.hedera.hapi.platform.state.PlatformState; import com.hedera.pbj.runtime.OneOf; @@ -232,9 +231,6 @@ private static OneOf queuePushChangeValueFor case PlatformState platformState -> { return new OneOf<>(SingletonUpdateChange.NewValueOneOfType.PLATFORM_STATE_VALUE, platformState); } - case TssStatus tssStatus -> { - return new OneOf<>(SingletonUpdateChange.NewValueOneOfType.TSS_STATUS_STATE_VALUE, tssStatus); - } default -> throw new IllegalArgumentException( "Unknown value type " + value.getClass().getName()); } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/KVStateChangeListener.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/KVStateChangeListener.java index fb16df8673bc..74fbf95e1899 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/KVStateChangeListener.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/KVStateChangeListener.java @@ -56,9 +56,9 @@ import com.hedera.hapi.node.state.token.StakingNodeInfo; import com.hedera.hapi.node.state.token.Token; import com.hedera.hapi.node.state.token.TokenRelation; +import com.hedera.hapi.node.state.tss.TssEncryptionKeys; import com.hedera.hapi.node.state.tss.TssMessageMapKey; import com.hedera.hapi.node.state.tss.TssVoteMapKey; -import com.hedera.hapi.services.auxiliary.tss.TssEncryptionKeyTransactionBody; import com.hedera.hapi.services.auxiliary.tss.TssMessageTransactionBody; import com.hedera.hapi.services.auxiliary.tss.TssVoteTransactionBody; import com.swirlds.state.StateChangeListener; @@ -241,8 +241,8 @@ private static MapChangeValue mapChangeValueFor(@NonNull final V value) { case TssVoteTransactionBody tssVoteTransactionBody -> MapChangeValue.newBuilder() .tssVoteValue(tssVoteTransactionBody) .build(); - case TssEncryptionKeyTransactionBody tssEncryptionKeyTransactionBody -> MapChangeValue.newBuilder() - .tssEncryptionKeyValue(tssEncryptionKeyTransactionBody) + case TssEncryptionKeys tssEncryptionKeys -> MapChangeValue.newBuilder() + .tssEncryptionKeysValue(tssEncryptionKeys) .build(); default -> throw new IllegalStateException( "Unexpected value: " + value.getClass().getSimpleName()); diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/schemas/V0560BlockStreamSchema.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/schemas/V0560BlockStreamSchema.java index ab8d8fb642f9..edc27b1d036b 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/schemas/V0560BlockStreamSchema.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/schemas/V0560BlockStreamSchema.java @@ -85,7 +85,7 @@ public V0560BlockStreamSchema(@NonNull final Consumer migratedBlockHashCo public void restart(@NonNull final MigrationContext ctx) { requireNonNull(ctx); final var state = ctx.newStates().getSingleton(BLOCK_STREAM_INFO_KEY); - if (ctx.previousVersion() == null) { + if (ctx.isGenesis()) { state.put(BlockStreamInfo.DEFAULT); } else { final var blockStreamInfo = state.get(); diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/fees/schemas/V0490FeeSchema.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/fees/schemas/V0490FeeSchema.java index efc01409d355..83829e8e8277 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/fees/schemas/V0490FeeSchema.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/fees/schemas/V0490FeeSchema.java @@ -52,7 +52,7 @@ public void migrate(@NonNull final MigrationContext ctx) { if (isGenesis) { // Set the initial exchange rates (from the bootstrap config) as the midnight rates final var midnightRatesState = ctx.newStates().getSingleton(MIDNIGHT_RATES_STATE_KEY); - final var bootstrapConfig = ctx.configuration().getConfigData(BootstrapConfig.class); + final var bootstrapConfig = ctx.appConfig().getConfigData(BootstrapConfig.class); final var exchangeRateSet = ExchangeRateSet.newBuilder() .currentRate(ExchangeRate.newBuilder() .centEquiv(bootstrapConfig.ratesCurrentCentEquiv()) diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/ids/schemas/V0490EntityIdSchema.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/ids/schemas/V0490EntityIdSchema.java index e63a8fd4196c..c38cc1d87031 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/ids/schemas/V0490EntityIdSchema.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/ids/schemas/V0490EntityIdSchema.java @@ -68,7 +68,7 @@ public Set statesToCreate() { public void migrate(@NonNull MigrationContext ctx) { final var entityIdState = ctx.newStates().getSingleton(ENTITY_ID_STATE_KEY); if (entityIdState.get() == null) { - final var config = ctx.configuration().getConfigData(HederaConfig.class); + final var config = ctx.appConfig().getConfigData(HederaConfig.class); final var entityNum = config.firstUserEntity() - 1; log.info("Setting initial entity id to {}", entityNum); entityIdState.put(new EntityNumber(entityNum)); diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/info/DiskStartupNetworks.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/info/DiskStartupNetworks.java index bd2cc7b10955..2df85c36d661 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/info/DiskStartupNetworks.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/info/DiskStartupNetworks.java @@ -16,10 +16,15 @@ package com.hedera.node.app.info; +import static com.hedera.hapi.util.HapiUtils.parseAccount; +import static com.swirlds.platform.roster.RosterRetriever.buildRoster; import static java.util.Objects.requireNonNull; +import com.hedera.hapi.node.base.Key; +import com.hedera.hapi.node.state.addressbook.Node; import com.hedera.hapi.node.state.roster.Roster; import com.hedera.hapi.node.state.roster.RosterEntry; +import com.hedera.hapi.node.state.tss.TssEncryptionKeys; import com.hedera.hapi.services.auxiliary.tss.TssMessageTransactionBody; import com.hedera.node.app.roster.RosterService; import com.hedera.node.app.service.addressbook.AddressBookService; @@ -37,9 +42,13 @@ import com.hedera.pbj.runtime.io.buffer.Bytes; import com.hedera.pbj.runtime.io.stream.ReadableStreamingData; import com.hedera.pbj.runtime.io.stream.WritableStreamingData; +import com.swirlds.common.platform.NodeId; import com.swirlds.config.api.Configuration; +import com.swirlds.platform.state.service.PlatformStateService; +import com.swirlds.platform.state.service.ReadablePlatformStateStore; import com.swirlds.platform.state.service.ReadableRosterStore; import com.swirlds.platform.state.service.ReadableRosterStoreImpl; +import com.swirlds.platform.system.address.AddressBook; import com.swirlds.state.State; import com.swirlds.state.lifecycle.StartupNetworks; import edu.umd.cs.findbugs.annotations.NonNull; @@ -62,41 +71,36 @@ public class DiskStartupNetworks implements StartupNetworks { private static final Logger log = LogManager.getLogger(DiskStartupNetworks.class); - private static final Pattern ROUND_DIR_PATTERN = Pattern.compile("\\d+"); - public static final String ARCHIVE = ".archive"; public static final String GENESIS_NETWORK_JSON = "genesis-network.json"; public static final String OVERRIDE_NETWORK_JSON = "override-network.json"; + public static final Pattern ROUND_DIR_PATTERN = Pattern.compile("\\d+"); - private final long selfNodeId; private final ConfigProvider configProvider; private final TssBaseService tssBaseService; private boolean isArchived = false; public DiskStartupNetworks( - final long selfNodeId, - @NonNull final ConfigProvider configProvider, - @NonNull final TssBaseService tssBaseService) { - this.selfNodeId = selfNodeId; + @NonNull final ConfigProvider configProvider, @NonNull final TssBaseService tssBaseService) { this.configProvider = requireNonNull(configProvider); this.tssBaseService = tssBaseService; } @Override public Network genesisNetworkOrThrow() { - return loadNetwork(configProvider.getConfiguration(), GENESIS_NETWORK_JSON) + return loadNetwork("genesis", configProvider.getConfiguration(), GENESIS_NETWORK_JSON) .orElseThrow(() -> new IllegalStateException("Genesis network not found")); } @Override public Optional overrideNetworkFor(final long roundNumber) { final var config = configProvider.getConfiguration(); - final var unscopedNetwork = loadNetwork(config, OVERRIDE_NETWORK_JSON); + final var unscopedNetwork = loadNetwork("override", config, OVERRIDE_NETWORK_JSON); if (unscopedNetwork.isPresent()) { return unscopedNetwork; } - return loadNetwork(config, "" + roundNumber, OVERRIDE_NETWORK_JSON); + return loadNetwork("override", config, "" + roundNumber, OVERRIDE_NETWORK_JSON); } @Override @@ -147,12 +151,13 @@ public void archiveStartupNetworks() { @Override public Network migrationNetworkOrThrow() { // FUTURE - look into sourcing this from a config.txt and public.pfx to ease migration - return loadNetwork(configProvider.getConfiguration(), OVERRIDE_NETWORK_JSON) + return loadNetwork("migration", configProvider.getConfiguration(), OVERRIDE_NETWORK_JSON) .orElseThrow(() -> new IllegalStateException("Transplant network not found")); } /** * Writes a JSON representation of the {@link Network} information in the given state to a given path. + * * @param state the state to write network information from. * @param path the path to write the JSON network information to. */ @@ -162,67 +167,133 @@ public static void writeNetworkInfo(@NonNull final State state, @NonNull final P new ReadableTssStoreImpl(state.getReadableStates(TssBaseService.NAME)), new ReadableNodeStoreImpl(state.getReadableStates(AddressBookService.NAME)), new ReadableRosterStoreImpl(state.getReadableStates(RosterService.NAME)), + new ReadablePlatformStateStore(state.getReadableStates(PlatformStateService.NAME)), path); } /** * Writes a JSON representation of the {@link Network} information in the given state to a given path. + * + * @param platformStateStore the platform state store to read the network information from * @param path the path to write the JSON network information to. */ public static void writeNetworkInfo( @NonNull final ReadableTssStore tssStore, @NonNull final ReadableNodeStore nodeStore, @NonNull final ReadableRosterStore rosterStore, + @NonNull final ReadablePlatformStateStore platformStateStore, @NonNull final Path path) { requireNonNull(tssStore); requireNonNull(nodeStore); requireNonNull(rosterStore); requireNonNull(path); - Optional.ofNullable(rosterStore.getActiveRoster()).ifPresent(activeRoster -> { - final var network = Network.newBuilder(); - final List nodeMetadata = new ArrayList<>(); - rosterStore.getActiveRoster().rosterEntries().forEach(entry -> { - final var node = requireNonNull(nodeStore.get(entry.nodeId())); - nodeMetadata.add(new NodeMetadata(entry, node, Bytes.EMPTY)); - }); - network.nodeMetadata(nodeMetadata); - final var sourceRosterHash = - Optional.ofNullable(rosterStore.getPreviousRosterHash()).orElse(Bytes.EMPTY); - tssStore.consensusRosterKeys( - sourceRosterHash, requireNonNull(rosterStore.getCurrentRosterHash()), rosterStore) - .ifPresent(rosterKeys -> - network.ledgerId(rosterKeys.ledgerId()).tssMessages(rosterKeys.tssMessages())); - try (final var fout = Files.newOutputStream(path)) { - Network.JSON.write(network.build(), new WritableStreamingData(fout)); - } catch (IOException e) { - log.warn("Failed to write network info", e); - } - }); + requireNonNull(platformStateStore); + Optional.ofNullable(rosterStore.getActiveRoster()) + .or(() -> Optional.ofNullable(buildRoster(platformStateStore.getAddressBook()))) + .ifPresent(activeRoster -> { + final var network = Network.newBuilder(); + final List nodeMetadata = new ArrayList<>(); + activeRoster.rosterEntries().forEach(entry -> { + final var node = requireNonNull(nodeStore.get(entry.nodeId())); + final var encryptionKey = Optional.ofNullable(tssStore.getTssEncryptionKeys(node.nodeId())) + .map(TssEncryptionKeys::currentEncryptionKey) + .orElse(Bytes.EMPTY); + nodeMetadata.add(new NodeMetadata(entry, node, encryptionKey)); + }); + network.nodeMetadata(nodeMetadata); + final var currentRosterHash = rosterStore.getCurrentRosterHash(); + if (currentRosterHash != null) { + final var sourceRosterHash = Optional.ofNullable(rosterStore.getPreviousRosterHash()) + .orElse(Bytes.EMPTY); + tssStore.consensusRosterKeys(sourceRosterHash, currentRosterHash, rosterStore) + .ifPresent(rosterKeys -> + network.ledgerId(rosterKeys.ledgerId()).tssMessages(rosterKeys.tssMessages())); + } + try (final var fout = Files.newOutputStream(path)) { + Network.JSON.write(network.build(), new WritableStreamingData(fout)); + } catch (IOException e) { + log.warn("Failed to write network info", e); + } + }); + } + + /** + * Converts a {@link AddressBook} to a {@link Network}. The resulting network will have no TSS + * keys of any kind. + * + * @param addressBook the address book to convert + * @return the converted network + */ + public static @NonNull Network fromLegacyAddressBook(@NonNull final AddressBook addressBook) { + final var roster = buildRoster(addressBook); + return Network.newBuilder() + .nodeMetadata(roster.rosterEntries().stream() + .map(rosterEntry -> { + final var nodeId = rosterEntry.nodeId(); + final var nodeAccountId = parseAccount( + addressBook.getAddress(NodeId.of(nodeId)).getMemo()); + // Currently the ReadableFreezeUpgradeActions.writeConfigLineAndPem() + // assumes that the gossip endpoints in the Node objects are in the order + // (Internal, External)...even though Roster format is the reverse :/ + final var legacyGossipEndpoints = List.of( + rosterEntry.gossipEndpoint().getLast(), + rosterEntry.gossipEndpoint().getFirst()); + return NodeMetadata.newBuilder() + .rosterEntry(rosterEntry) + .node(Node.newBuilder() + .nodeId(nodeId) + .accountId(nodeAccountId) + .description("node" + (nodeId + 1)) + .gossipEndpoint(legacyGossipEndpoints) + .serviceEndpoint(List.of()) + .gossipCaCertificate(rosterEntry.gossipCaCertificate()) + .grpcCertificateHash(Bytes.EMPTY) + .weight(rosterEntry.weight()) + .deleted(false) + .adminKey(Key.DEFAULT) + .build()) + .tssEncryptionKey(Bytes.EMPTY) + .build(); + }) + .toList()) + .build(); } /** * Attempts to load a {@link Network} from a given file in the directory whose relative path is given * by the provided {@link Configuration}. + * + * @param type the type of network to load * @param config the configuration to use to determine the location of the network file * @param segments the path segments of the file to load the network from * @return the loaded network, if it was found and successfully loaded */ - private Optional loadNetwork(@NonNull final Configuration config, @NonNull final String... segments) { + private Optional loadNetwork( + @NonNull final String type, @NonNull final Configuration config, @NonNull final String... segments) { final var path = networksPath(config, segments); + log.info("Loading {} network info from {}", type, path.toAbsolutePath()); if (Files.exists(path)) { try (final var fin = Files.newInputStream(path)) { final var network = Network.JSON.parse(new ReadableStreamingData(fin)); + log.info( + "Parsed {} network info for N={} nodes from {}", + type, + network.nodeMetadata().size(), + path.toAbsolutePath()); assertValidTssKeys(network); return Optional.of(network); } catch (Exception e) { - log.warn("Failed to load network info from {}", path.toAbsolutePath(), e); + log.warn("Failed to load {} network info from {}", path.toAbsolutePath(), e); } } return Optional.empty(); } /** - * If the given network has a ledger id, then it asserts that the TSS keys in the network are valid. + * If the given network has a ledger id, then it asserts that the TSS keys in the network are valid. This includes + * the encryption keys within the {@link NodeMetadata} messages, since without these specified the TSS messages + * would be unusable. + * * @param network the network to assert the TSS keys of * @throws IllegalArgumentException if the TSS keys are invalid */ @@ -240,7 +311,8 @@ private void assertValidTssKeys(@NonNull final Network network) { .getConfiguration() .getConfigData(TssConfig.class) .maxSharesPerNode(); - final var directory = TssUtils.computeParticipantDirectory(roster, maxSharesPerNode); + final var encryptionKeysFn = TssUtils.encryptionKeysFnFor(network); + final var directory = TssUtils.computeParticipantDirectory(roster, maxSharesPerNode, encryptionKeysFn); final var tssMessages = network.tssMessages().stream() .map(TssMessageTransactionBody::tssMessage) .map(Bytes::toByteArray) @@ -256,6 +328,7 @@ private void assertValidTssKeys(@NonNull final Network network) { /** * Attempts to archive the given segments in the given configuration. + * * @param segments the segments to archive */ private static void archiveIfPresent(@NonNull final Configuration config, @NonNull final String... segments) { @@ -275,6 +348,7 @@ private static void archiveIfPresent(@NonNull final Configuration config, @NonNu /** * Ensures that the archive directory exists in the given configuration. + * * @param config the configuration to ensure the archive directory exists in */ private static void ensureArchiveDir(@NonNull final Configuration config) throws IOException { @@ -283,6 +357,7 @@ private static void ensureArchiveDir(@NonNull final Configuration config) throws /** * Creates the given path as a directory if it does not already exist. + * * @param path the path to the directory create if it does not already exist */ private static void createIfAbsent(@NonNull final Path path) throws IOException { @@ -293,6 +368,7 @@ private static void createIfAbsent(@NonNull final Path path) throws IOException /** * Gets the path to the directory containing network files. + * * @param config the configuration to use to determine the location of the network files * @return the path to the directory containing network files */ diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/info/GenesisNetworkInfo.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/info/GenesisNetworkInfo.java index b6cbd3291846..e34ce5800a0f 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/info/GenesisNetworkInfo.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/info/GenesisNetworkInfo.java @@ -16,19 +16,15 @@ package com.hedera.node.app.info; -import static com.hedera.node.app.service.token.impl.handlers.BaseCryptoHandler.asAccount; import static java.util.Objects.requireNonNull; -import com.hedera.hapi.node.state.roster.Roster; -import com.hedera.hapi.node.state.roster.RosterEntry; +import com.hedera.node.internal.network.Network; import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.state.State; import com.swirlds.state.lifecycle.info.NetworkInfo; import com.swirlds.state.lifecycle.info.NodeInfo; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import java.util.ArrayList; -import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -38,19 +34,17 @@ */ public class GenesisNetworkInfo implements NetworkInfo { private final Bytes ledgerId; - private final Roster genesisRoster; private final Map nodeInfos; /** * Constructs a new {@link GenesisNetworkInfo} instance. * - * @param genesisRoster The genesis roster + * @param genesisNetwork The genesis network * @param ledgerId The ledger ID */ - public GenesisNetworkInfo(@NonNull final Roster genesisRoster, @NonNull final Bytes ledgerId) { + public GenesisNetworkInfo(@NonNull final Network genesisNetwork, @NonNull final Bytes ledgerId) { this.ledgerId = requireNonNull(ledgerId); - this.genesisRoster = requireNonNull(genesisRoster); - this.nodeInfos = buildNodeInfoMap(genesisRoster); + this.nodeInfos = nodeInfosFrom(genesisNetwork); } /** @@ -102,50 +96,18 @@ public void updateFrom(final State state) { throw new UnsupportedOperationException("Not implemented"); } - @Override - public Roster roster() { - return genesisRoster; - } - - /** - * Builds a map of node information from the given roster. The map is keyed by node ID. - * The node information is retrieved from the roster entry. - * If the node information is not found in the roster entry, it is not included in the map. - * - * @param roster The roster to retrieve the node information from - * @return A map of node information - */ - private Map buildNodeInfoMap(@NonNull final Roster roster) { + private static Map nodeInfosFrom(@NonNull final Network network) { final var nodeInfos = new LinkedHashMap(); - final var rosterEntries = roster.rosterEntries(); - for (final var rosterEntry : rosterEntries) { - nodeInfos.put(rosterEntry.nodeId(), fromRosterEntry(rosterEntry)); + for (final var metadata : network.nodeMetadata()) { + final var node = metadata.nodeOrThrow(); + final var nodeInfo = new NodeInfoImpl( + node.nodeId(), + node.accountIdOrThrow(), + node.weight(), + node.gossipEndpoint(), + node.gossipCaCertificate()); + nodeInfos.put(node.nodeId(), nodeInfo); } return nodeInfos; } - - /** - * Builds a node info from a roster entry from the given roster. - * Since this is only used in the genesis case, the account ID is generated from the node ID - * by adding 3 to it, as a default case. - * - * @param entry The roster entry - * @return The node info - */ - private NodeInfo fromRosterEntry(@NonNull final RosterEntry entry) { - // The RosterEntry has external ip address at index 0. - // The NodeInfo needs to have internal ip address at index 0. - // swap the internal and external endpoints when converting to NodeInfo. - final var gossipEndpointsCopy = new ArrayList<>(entry.gossipEndpoint()); - if (gossipEndpointsCopy.size() > 1) { - Collections.swap(gossipEndpointsCopy, 0, 1); - } - - return new NodeInfoImpl( - entry.nodeId(), - asAccount(entry.nodeId() + 3), - entry.weight(), - gossipEndpointsCopy, - entry.gossipCaCertificate()); - } } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/info/NodeInfoImpl.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/info/NodeInfoImpl.java index eddf0fdd3561..c61ab3010ded 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/info/NodeInfoImpl.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/info/NodeInfoImpl.java @@ -29,7 +29,7 @@ public record NodeInfoImpl( long nodeId, @NonNull AccountID accountId, - long stake, + long weight, List gossipEndpoints, @Nullable Bytes sigCertBytes) implements NodeInfo { diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/info/StateNetworkInfo.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/info/StateNetworkInfo.java index 82532d9768b1..283e6a90576b 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/info/StateNetworkInfo.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/info/StateNetworkInfo.java @@ -18,8 +18,6 @@ import static com.hedera.node.app.info.NodeInfoImpl.fromRosterEntry; import static com.hedera.node.app.service.addressbook.impl.schemas.V053AddressBookSchema.NODES_KEY; -import static com.swirlds.platform.roster.RosterRetriever.buildRoster; -import static com.swirlds.platform.roster.RosterRetriever.retrieveActiveOrGenesisRoster; import static java.util.Objects.requireNonNull; import com.hedera.hapi.node.base.AccountID; @@ -29,10 +27,7 @@ import com.hedera.node.app.service.addressbook.AddressBookService; import com.hedera.node.config.ConfigProvider; import com.hedera.node.config.data.LedgerConfig; -import com.hedera.node.config.data.TssConfig; import com.hedera.pbj.runtime.io.buffer.Bytes; -import com.swirlds.platform.state.service.PlatformStateService; -import com.swirlds.platform.state.service.ReadablePlatformStateStore; import com.swirlds.state.State; import com.swirlds.state.lifecycle.info.NetworkInfo; import com.swirlds.state.lifecycle.info.NodeInfo; @@ -56,32 +51,35 @@ public class StateNetworkInfo implements NetworkInfo { private static final Logger log = LogManager.getLogger(StateNetworkInfo.class); private final long selfId; private final Bytes ledgerId; - private Roster activeRoster; - private final ConfigProvider configProvider; + /** + * The active roster, used to limit exposed node info to the active set of nodes. + */ + private final Roster activeRoster; + private final Map nodeInfos; /** * Constructs a new network information provider from the given state, roster, selfID, and configuration provider. * - * @param state the state to retrieve the network information from - * @param roster the roster to retrieve the network information from - * @param selfId the ID of the node + * @param selfId the ID of the node + * @param state the state to retrieve the network information from + * @param roster the roster to retrieve the network information from * @param configProvider the configuration provider to retrieve the ledger ID from */ public StateNetworkInfo( + final long selfId, @NonNull final State state, @NonNull final Roster roster, - final long selfId, @NonNull final ConfigProvider configProvider) { - this.selfId = selfId; + requireNonNull(state); + requireNonNull(configProvider); this.activeRoster = requireNonNull(roster); - // We keep this for now to check the keyCandidateRoster feature flag in updateFrom() - this.configProvider = requireNonNull(configProvider); - this.nodeInfos = buildNodeInfoMap(state); - // Load the ledger ID from configuration - final var config = configProvider.getConfiguration(); - final var ledgerConfig = config.getConfigData(LedgerConfig.class); - ledgerId = ledgerConfig.id(); + this.ledgerId = configProvider + .getConfiguration() + .getConfigData(LedgerConfig.class) + .id(); + this.nodeInfos = nodeInfosFrom(state); + this.selfId = selfId; } @NonNull @@ -115,19 +113,8 @@ public boolean containsNode(final long nodeId) { @Override public void updateFrom(@NonNull final State state) { - final var config = configProvider.getConfiguration(); - if (config.getConfigData(TssConfig.class).keyCandidateRoster()) { - activeRoster = retrieveActiveOrGenesisRoster(state); - } else { - // When the feature flag is disabled, the rosters in RosterService state are not up-to-date - // FUTURE: Once TSS Roster is implemented in the future, this will be removed and use roster state - // instead of the address book - final var readablePlatformStateStore = - new ReadablePlatformStateStore(state.getReadableStates(PlatformStateService.NAME)); - activeRoster = buildRoster(requireNonNull(readablePlatformStateStore.getAddressBook())); - } nodeInfos.clear(); - nodeInfos.putAll(buildNodeInfoMap(state)); + nodeInfos.putAll(nodeInfosFrom(state)); } /** @@ -138,16 +125,15 @@ public void updateFrom(@NonNull final State state) { * @param state the state to retrieve the node information from * @return a map of node information */ - private Map buildNodeInfoMap(final State state) { - final var nodeInfos = new LinkedHashMap(); - final var rosterEntries = activeRoster.rosterEntries(); - final ReadableKVState nodeState = + private Map nodeInfosFrom(@NonNull final State state) { + final ReadableKVState nodes = state.getReadableStates(AddressBookService.NAME).get(NODES_KEY); - for (final var rosterEntry : rosterEntries) { + final Map nodeInfos = new LinkedHashMap<>(); + for (final var rosterEntry : activeRoster.rosterEntries()) { // At genesis the node store is derived from the roster, hence must have info for every // node id; and from then on, the roster is derived from the node store, and hence the // node store must have every node id in the roster. - final var node = nodeState.get(new EntityNumber(rosterEntry.nodeId())); + final var node = nodes.get(new EntityNumber(rosterEntry.nodeId())); if (node != null) { // Notice it's possible the node could be deleted here, because a DAB transaction removed // it from the future address book; that doesn't mean we should stop using it in the current @@ -166,9 +152,4 @@ private Map buildNodeInfoMap(final State state) { } return nodeInfos; } - - @Override - public Roster roster() { - return activeRoster; - } } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/info/UnavailableNetworkInfo.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/info/UnavailableNetworkInfo.java index 87fb87732f66..692700ce6e98 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/info/UnavailableNetworkInfo.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/info/UnavailableNetworkInfo.java @@ -16,7 +16,6 @@ package com.hedera.node.app.info; -import com.hedera.hapi.node.state.roster.Roster; import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.state.State; import com.swirlds.state.lifecycle.info.NetworkInfo; @@ -66,9 +65,4 @@ public boolean containsNode(final long nodeId) { public void updateFrom(final State state) { throw new UnsupportedOperationException("Not implemented"); } - - @Override - public Roster roster() { - throw new UnsupportedOperationException("Not implemented"); - } } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/roster/RosterService.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/roster/RosterService.java index 37e67a8e1f4a..470cc01d0bcd 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/roster/RosterService.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/roster/RosterService.java @@ -20,14 +20,14 @@ import static java.util.Objects.requireNonNull; import com.hedera.hapi.node.state.roster.Roster; -import com.hedera.node.app.roster.schemas.V057RosterSchema; +import com.hedera.node.app.roster.schemas.V0540RosterSchema; import com.swirlds.platform.state.service.ReadablePlatformStateStore; import com.swirlds.platform.state.service.WritableRosterStore; -import com.swirlds.platform.state.service.schemas.V0540RosterSchema; import com.swirlds.state.lifecycle.SchemaRegistry; import com.swirlds.state.lifecycle.Service; import edu.umd.cs.findbugs.annotations.NonNull; import java.util.function.Predicate; +import java.util.function.Supplier; /** * A {@link com.hedera.hapi.node.state.roster.Roster} implementation of the {@link Service} interface. @@ -49,9 +49,18 @@ public class RosterService implements Service { * adopted at an upgrade boundary. */ private final Predicate canAdopt; + /** + * Required until the upgrade that adopts the roster lifecycle; at that upgrade boundary, + * we must initialize the active roster from the platform state's legacy address books. + */ + @Deprecated + private final Supplier platformStateStoreFactory; - public RosterService(@NonNull final Predicate canAdopt) { + public RosterService( + @NonNull final Predicate canAdopt, + @NonNull final Supplier platformStateStoreFactory) { this.canAdopt = requireNonNull(canAdopt); + this.platformStateStoreFactory = requireNonNull(platformStateStoreFactory); } @NonNull @@ -68,7 +77,6 @@ public int migrationOrder() { @Override public void registerSchemas(@NonNull final SchemaRegistry registry) { requireNonNull(registry); - registry.register(new V0540RosterSchema()); - registry.register(new V057RosterSchema(canAdopt, WritableRosterStore::new, ReadablePlatformStateStore::new)); + registry.register(new V0540RosterSchema(canAdopt, WritableRosterStore::new, platformStateStoreFactory)); } } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/roster/schemas/RosterTransplantSchema.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/roster/schemas/RosterTransplantSchema.java new file mode 100644 index 000000000000..0259532b9872 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/roster/schemas/RosterTransplantSchema.java @@ -0,0 +1,66 @@ +/* + * 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.roster.schemas; + +import static java.util.Objects.requireNonNull; + +import com.hedera.node.app.roster.RosterService; +import com.swirlds.platform.config.AddressBookConfig; +import com.swirlds.platform.roster.RosterUtils; +import com.swirlds.platform.state.service.WritableRosterStore; +import com.swirlds.state.lifecycle.MigrationContext; +import com.swirlds.state.lifecycle.Schema; +import com.swirlds.state.spi.WritableStates; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.function.Function; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * The {@link Schema#restart(MigrationContext)} implementation whereby the {@link RosterService} ensures that any + * roster overrides in the startup assets are copied into the state. + *

    + * Important: The latest {@link RosterService} schema should always implement this interface. + */ +public interface RosterTransplantSchema { + Logger log = LogManager.getLogger(RosterTransplantSchema.class); + + /** + * Restart the {@link RosterService} by copying any roster overrides from the startup assets into the state. + * @param ctx the migration context + * @param rosterStoreFactory the factory to use to create the writable roster store + */ + default boolean restart( + @NonNull final MigrationContext ctx, + @NonNull final Function rosterStoreFactory) { + requireNonNull(ctx); + if (ctx.appConfig().getConfigData(AddressBookConfig.class).useRosterLifecycle()) { + final long roundNumber = ctx.roundNumber(); + final var startupNetworks = ctx.startupNetworks(); + final var overrideNetwork = startupNetworks.overrideNetworkFor(roundNumber); + overrideNetwork.ifPresent(network -> { + final long activeRoundNumber = roundNumber + 1; + log.info("Adopting roster from override network in round {}", activeRoundNumber); + final var rosterStore = rosterStoreFactory.apply(ctx.newStates()); + rosterStore.putActiveRoster(RosterUtils.rosterFrom(network), activeRoundNumber); + startupNetworks.setOverrideRound(roundNumber); + }); + return overrideNetwork.isPresent(); + } + return false; + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/roster/schemas/V0540RosterSchema.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/roster/schemas/V0540RosterSchema.java new file mode 100644 index 000000000000..4c2220218178 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/roster/schemas/V0540RosterSchema.java @@ -0,0 +1,134 @@ +/* + * 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.roster.schemas; + +import static com.swirlds.platform.roster.RosterRetriever.buildRoster; +import static java.util.Objects.requireNonNull; + +import com.hedera.hapi.node.base.SemanticVersion; +import com.hedera.hapi.node.state.roster.Roster; +import com.hedera.hapi.node.state.roster.RosterState; +import com.hedera.node.app.version.ServicesSoftwareVersion; +import com.swirlds.platform.config.AddressBookConfig; +import com.swirlds.platform.roster.RosterUtils; +import com.swirlds.platform.state.service.ReadablePlatformStateStore; +import com.swirlds.platform.state.service.WritableRosterStore; +import com.swirlds.platform.state.service.schemas.V0540RosterBaseSchema; +import com.swirlds.state.lifecycle.MigrationContext; +import com.swirlds.state.lifecycle.Schema; +import com.swirlds.state.lifecycle.StateDefinition; +import com.swirlds.state.spi.WritableStates; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Initial {@link com.hedera.node.app.roster.RosterService} schema that registers two states, + *

      + *
    1. A mapping from roster hashes to rosters (which may be either candidate or active).
    2. + *
    3. A singleton that contains the history of active rosters along with the round numbers where + * they were adopted; along with the hash of a candidate roster if there is one.
    4. + *
    + */ +public class V0540RosterSchema extends Schema implements RosterTransplantSchema { + private static final Logger log = LogManager.getLogger(V0540RosterSchema.class); + + public static final String ROSTER_KEY = "ROSTERS"; + public static final String ROSTER_STATES_KEY = "ROSTER_STATE"; + + private static final SemanticVersion VERSION = + SemanticVersion.newBuilder().major(0).minor(54).patch(0).build(); + + /** + * The delegate schema that defines the base roster schema. + */ + private final V0540RosterBaseSchema baseSchema = new V0540RosterBaseSchema(); + /** + * The test to use to determine if a candidate roster may be adopted at an upgrade boundary. + */ + private final Predicate canAdopt; + /** + * The factory to use to create the writable roster store. + */ + private final Function rosterStoreFactory; + /** + * Required until the upgrade that adopts the roster lifecycle; at that upgrade boundary, + * we must initialize the active roster from the platform state's legacy address books. + */ + @Deprecated + private final Supplier platformStateStoreFactory; + + public V0540RosterSchema( + @NonNull final Predicate canAdopt, + @NonNull final Function rosterStoreFactory, + @NonNull final Supplier platformStateStoreFactory) { + super(VERSION); + this.canAdopt = requireNonNull(canAdopt); + this.rosterStoreFactory = requireNonNull(rosterStoreFactory); + this.platformStateStoreFactory = requireNonNull(platformStateStoreFactory); + } + + @Override + public @NonNull Set statesToCreate() { + return baseSchema.statesToCreate(); + } + + @Override + public void migrate(@NonNull final MigrationContext ctx) { + requireNonNull(ctx); + final var rosterState = ctx.newStates().getSingleton(ROSTER_STATES_KEY); + if (!ctx.appConfig().getConfigData(AddressBookConfig.class).useRosterLifecycle()) { + rosterState.put(RosterState.DEFAULT); + } + } + + @Override + public void restart(@NonNull final MigrationContext ctx) { + requireNonNull(ctx); + if (!RosterTransplantSchema.super.restart(ctx, rosterStoreFactory) + && ctx.appConfig().getConfigData(AddressBookConfig.class).useRosterLifecycle()) { + final var startupNetworks = ctx.startupNetworks(); + final var rosterStore = rosterStoreFactory.apply(ctx.newStates()); + final var activeRoundNumber = ctx.roundNumber() + 1; + if (ctx.isGenesis()) { + rosterStore.putActiveRoster(RosterUtils.rosterFrom(startupNetworks.genesisNetworkOrThrow()), 0L); + } else if (rosterStore.getActiveRoster() == null) { + // (FUTURE) Once the roster lifecycle is active by default, remove this code building an initial + // roster history from the last address book and the first roster at the upgrade boundary + final var addressBook = platformStateStoreFactory.get().getAddressBook(); + final var previousRoster = buildRoster(requireNonNull(addressBook)); + rosterStore.putActiveRoster(previousRoster, 0); + final var currentRoster = RosterUtils.rosterFrom(startupNetworks.migrationNetworkOrThrow()); + rosterStore.putActiveRoster(currentRoster, activeRoundNumber); + } else if (ctx.isUpgrade(ServicesSoftwareVersion::from, ServicesSoftwareVersion::new)) { + final var candidateRoster = rosterStore.getCandidateRoster(); + if (candidateRoster == null) { + log.info("No candidate roster to adopt in round {}", activeRoundNumber); + } else if (canAdopt.test(candidateRoster)) { + log.info("Adopting candidate roster in round {}", activeRoundNumber); + rosterStore.adoptCandidateRoster(activeRoundNumber); + } else { + log.info("Rejecting candidate roster in round {}", activeRoundNumber); + } + } + } + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/roster/schemas/V057RosterSchema.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/roster/schemas/V057RosterSchema.java deleted file mode 100644 index 5af1663e3962..000000000000 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/roster/schemas/V057RosterSchema.java +++ /dev/null @@ -1,156 +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.roster.schemas; - -import static com.swirlds.platform.roster.RosterRetriever.buildRoster; -import static java.util.Objects.requireNonNull; - -import com.hedera.hapi.node.base.SemanticVersion; -import com.hedera.hapi.node.state.roster.Roster; -import com.hedera.node.app.version.ServicesSoftwareVersion; -import com.hedera.node.internal.network.Network; -import com.hedera.node.internal.network.NodeMetadata; -import com.swirlds.platform.config.AddressBookConfig; -import com.swirlds.platform.state.service.ReadablePlatformStateStore; -import com.swirlds.platform.state.service.WritableRosterStore; -import com.swirlds.state.lifecycle.MigrationContext; -import com.swirlds.state.lifecycle.Schema; -import com.swirlds.state.spi.WritableStates; -import edu.umd.cs.findbugs.annotations.NonNull; -import java.util.function.Function; -import java.util.function.Predicate; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -/** - * A restart-only schema that ensures state has a current roster if {@link AddressBookConfig#useRosterLifecycle()} is set. - */ -public class V057RosterSchema extends Schema { - private static final Logger log = LogManager.getLogger(V057RosterSchema.class); - - private static final long GENESIS_ROUND_NO = 0L; - private static final SemanticVersion VERSION = - SemanticVersion.newBuilder().major(0).minor(57).build(); - - /** - * The test to use to determine if a candidate roster may be - * adopted at an upgrade boundary. - */ - private final Predicate canAdopt; - /** - * The factory to use to create the writable roster store. - */ - private final Function rosterStoreFactory; - - private final Function platformStateStoreFactory; - - public V057RosterSchema( - @NonNull final Predicate canAdopt, - @NonNull final Function rosterStoreFactory, - @NonNull final Function platformStateStoreFactory) { - super(VERSION); - this.canAdopt = requireNonNull(canAdopt); - this.rosterStoreFactory = requireNonNull(rosterStoreFactory); - this.platformStateStoreFactory = requireNonNull(platformStateStoreFactory); - } - - @Override - public void restart(@NonNull final MigrationContext ctx) { - requireNonNull(ctx); - if (!ctx.configuration().getConfigData(AddressBookConfig.class).useRosterLifecycle()) { - return; - } - final var rosterStore = rosterStoreFactory.apply(ctx.newStates()); - final var startupNetworks = ctx.startupNetworks(); - if (ctx.isGenesis()) { - setActiveRoster(GENESIS_ROUND_NO, rosterStore, startupNetworks.genesisNetworkOrThrow()); - } else { - final long roundNumber = ctx.roundNumber(); - final var overrideNetwork = startupNetworks.overrideNetworkFor(roundNumber); - if (overrideNetwork.isPresent()) { - // currentRound := state round +1 - final long currentRound = roundNumber + 1; - log.info("Found override network for round {}", currentRound); - - // If there is no active roster in the roster state. - if (rosterStore.getActiveRoster() == null) { - // Read the current AddressBooks from the platform state. - // previousRoster := translateToRoster(currentAddressBook) - final var platformState = platformStateStoreFactory.apply(ctx.newStates()); - final var previousRoster = buildRoster(platformState.getAddressBook()); - // (previousRoster, previousRound) := (previousRoster, 0) - // set (previousRoster, 0) as the active roster in the roster state. - rosterStore.putActiveRoster(previousRoster, 0L); - } - - // set (overrideRoster, currentRound) as the active roster in the roster state. - setActiveRoster(currentRound, rosterStore, overrideNetwork.get()); - startupNetworks.setOverrideRound(roundNumber); - } else if (isUpgrade(ctx)) { - if (rosterStore.getActiveRoster() == null) { - // currentRound := state round +1 - final long currentRound = roundNumber + 1; - log.info("Migrating active roster at round {}", currentRound); - - // Read the current AddressBooks from the platform state. - // previousRoster := translateToRoster(currentAddressBook) - // set (previousRoster, 0) as the active roster in the roster state. - final var platformState = platformStateStoreFactory.apply(ctx.newStates()); - final var previousRoster = buildRoster(platformState.getAddressBook()); - rosterStore.putActiveRoster(previousRoster, 0L); - - // If there is no active roster at a migration boundary, we - // must have a migration network in the startup assets - // configAddressBook := Read the address book in config.txt - final var network = startupNetworks.migrationNetworkOrThrow(); - - // currentRoster := translateToRoster(configAddressBook) - // set (currentRoster, currentRound) as the active roster in the roster state. - rosterStore.putActiveRoster(rosterFrom(network), currentRound); - } else { - // candidateRoster := read the candidate roster from the roster state. - final var candidateRoster = rosterStore.getCandidateRoster(); - if (canAdopt.test(candidateRoster)) { - // currentRound := state round +1 - final long currentRound = roundNumber + 1; - log.info("Adopting candidate roster at round {}", currentRound); - - // set (candidateRoster, currentRound) as the new active roster in the roster state. - rosterStore.adoptCandidateRoster(currentRound); - } - } - } - } - } - - private void setActiveRoster( - final long roundNumber, @NonNull final WritableRosterStore rosterStore, @NonNull final Network network) { - rosterStore.putActiveRoster(rosterFrom(network), roundNumber); - } - - private Roster rosterFrom(@NonNull final Network network) { - return new Roster(network.nodeMetadata().stream() - .map(NodeMetadata::rosterEntryOrThrow) - .toList()); - } - - private boolean isUpgrade(@NonNull final MigrationContext ctx) { - return ServicesSoftwareVersion.from(ctx.configuration()) - .compareTo(new ServicesSoftwareVersion(requireNonNull(ctx.previousVersion()))) - > 0; - } -} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/MigrationContextImpl.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/MigrationContextImpl.java index ebb5c549e287..92fa458ff1e7 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/MigrationContextImpl.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/MigrationContextImpl.java @@ -37,7 +37,7 @@ * * @param previousStates The previous states. * @param newStates The new states, preloaded with any new state definitions. - * @param configuration The configuration to use + * @param appConfig The configuration to use * @param genesisNetworkInfo The genesis network info * @param writableEntityIdStore The instance responsible for generating new entity IDs (ONLY during * migrations). Note that this is nullable only because it cannot exist @@ -47,7 +47,8 @@ public record MigrationContextImpl( @NonNull ReadableStates previousStates, @NonNull WritableStates newStates, - @NonNull Configuration configuration, + @NonNull Configuration appConfig, + @NonNull Configuration platformConfig, @Nullable NetworkInfo genesisNetworkInfo, @Nullable WritableEntityIdStore writableEntityIdStore, @Nullable SemanticVersion previousVersion, @@ -58,7 +59,8 @@ public record MigrationContextImpl( public MigrationContextImpl { requireNonNull(previousStates); requireNonNull(newStates); - requireNonNull(configuration); + requireNonNull(appConfig); + requireNonNull(platformConfig); } @Override diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/OrderedServiceMigrator.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/OrderedServiceMigrator.java index 302c60c8fb36..89a5de531bd7 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/OrderedServiceMigrator.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/OrderedServiceMigrator.java @@ -64,8 +64,8 @@ public class OrderedServiceMigrator implements ServiceMigrator { * @param servicesRegistry The services registry to use for the migrations * @param previousVersion The previous version of the state * @param currentVersion The current version of the state - * @param nodeConfiguration The system configuration to use at the time of migration - * @param platformConfiguration The platform configuration to use for subsequent object initializations + * @param appConfig The system configuration to use at the time of migration + * @param platformConfig The platform configuration to use for subsequent object initializations * @param genesisNetworkInfo The network information to use for the migrations. * This is only used in genesis case * @param metrics The metrics to use for the migrations @@ -78,19 +78,19 @@ public List doMigrations( @NonNull final ServicesRegistry servicesRegistry, @Nullable final SoftwareVersion previousVersion, @NonNull final SoftwareVersion currentVersion, - @NonNull final Configuration nodeConfiguration, - @NonNull final Configuration platformConfiguration, + @NonNull final Configuration appConfig, + @NonNull final Configuration platformConfig, @Nullable final NetworkInfo genesisNetworkInfo, @NonNull final Metrics metrics, @NonNull final StartupNetworks startupNetworks) { requireNonNull(state); requireNonNull(currentVersion); - requireNonNull(nodeConfiguration); - requireNonNull(platformConfiguration); + requireNonNull(appConfig); + requireNonNull(platformConfig); requireNonNull(metrics); final Map sharedValues = new HashMap<>(); - final var migrationStateChanges = new MigrationStateChanges(state, nodeConfiguration); + final var migrationStateChanges = new MigrationStateChanges(state, appConfig); logger.info("Migrating Entity ID Service as pre-requisite for other services"); final var entityIdRegistration = servicesRegistry.registrations().stream() .filter(service -> EntityIdService.NAME.equals(service.service().getServiceName())) @@ -104,8 +104,8 @@ public List doMigrations( state, deserializedPbjVersion, currentVersion.getPbjSemanticVersion(), - nodeConfiguration, - platformConfiguration, + appConfig, + platformConfig, genesisNetworkInfo, metrics, // We call with null here because we're migrating the entity ID service itself @@ -143,8 +143,8 @@ public List doMigrations( state, deserializedPbjVersion, currentVersion.getPbjSemanticVersion(), - nodeConfiguration, - platformConfiguration, + appConfig, + platformConfig, genesisNetworkInfo, metrics, entityIdStore, diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/ServiceMigrator.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/ServiceMigrator.java index 949995a84766..eb3ede447ec3 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/ServiceMigrator.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/ServiceMigrator.java @@ -41,8 +41,8 @@ public interface ServiceMigrator { * @param servicesRegistry The services registry to use for the migrations * @param previousVersion The previous version of the state * @param currentVersion The current version of the state - * @param nodeConfiguration The configuration to use for the migrations - * @param platformConfiguration The platform configuration to use for subsequent object initializations + * @param appConfig The app configuration to use for the migrations + * @param platformConfig The platform configuration to use for subsequent object initializations * @param genesisNetworkInfo The network information to use for the migrations * @param metrics The metrics to use for the migrations * @param startupNetworks The startup networks to use for the migrations @@ -53,8 +53,8 @@ List doMigrations( @NonNull ServicesRegistry servicesRegistry, @Nullable SoftwareVersion previousVersion, @NonNull SoftwareVersion currentVersion, - @NonNull Configuration nodeConfiguration, - @NonNull Configuration platformConfiguration, + @NonNull Configuration appConfig, + @NonNull Configuration platformConfig, @Nullable NetworkInfo genesisNetworkInfo, @NonNull Metrics metrics, @NonNull StartupNetworks startupNetworks); diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/merkle/MerkleSchemaRegistry.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/merkle/MerkleSchemaRegistry.java index ae4872e198b9..c111793b24c7 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/merkle/MerkleSchemaRegistry.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/merkle/MerkleSchemaRegistry.java @@ -174,8 +174,8 @@ private record RedefinedWritableStates(WritableStates beforeStates, WritableStat * @param previousVersion The version of state loaded from disk. Possibly null. * @param currentVersion The current version. Never null. Must be newer than {@code * previousVersion}. - * @param nodeConfiguration The system configuration to use at the time of migration - * @param platformConfiguration The platform configuration to use for subsequent object initializations + * @param appConfig The system configuration to use at the time of migration + * @param platformConfig The platform configuration to use for subsequent object initializations * @param genesisNetworkInfo The network information to use at the time of migration * @param sharedValues A map of shared values for cross-service migration patterns * @param migrationStateChanges Tracker for state changes during migration @@ -189,8 +189,8 @@ public void migrate( @NonNull final State state, @Nullable final SemanticVersion previousVersion, @NonNull final SemanticVersion currentVersion, - @NonNull final Configuration nodeConfiguration, - @NonNull final Configuration platformConfiguration, + @NonNull final Configuration appConfig, + @NonNull final Configuration platformConfig, @Nullable final NetworkInfo genesisNetworkInfo, @NonNull final Metrics metrics, @Nullable final WritableEntityIdStore entityIdStore, @@ -199,8 +199,8 @@ public void migrate( @NonNull final StartupNetworks startupNetworks) { requireNonNull(state); requireNonNull(currentVersion); - requireNonNull(nodeConfiguration); - requireNonNull(platformConfiguration); + requireNonNull(appConfig); + requireNonNull(platformConfig); requireNonNull(metrics); requireNonNull(sharedValues); requireNonNull(migrationStateChanges); @@ -226,7 +226,7 @@ public void migrate( () -> HapiUtils.toString(latestVersion)); for (final var schema : schemas) { final var applications = - schemaApplications.computeApplications(previousVersion, latestVersion, schema, nodeConfiguration); + schemaApplications.computeApplications(previousVersion, latestVersion, schema, appConfig); logger.info("Applying {} schema {} ({})", serviceName, schema.getVersion(), applications); // Now we can migrate the schema and then commit all the changes // We just have one merkle tree -- the just-loaded working tree -- to work from. @@ -251,7 +251,7 @@ public void migrate( && alreadyIncludesStateDefs(previousVersion, s.getVersion())) .toList(); final var redefinedWritableStates = applyStateDefinitions( - schema, schemasAlreadyInState, nodeConfiguration, platformConfiguration, metrics, stateRoot); + schema, schemasAlreadyInState, appConfig, platformConfig, metrics, stateRoot); writableStates = redefinedWritableStates.beforeStates(); newStates = redefinedWritableStates.afterStates(); } else { @@ -261,7 +261,8 @@ && alreadyIncludesStateDefs(previousVersion, s.getVersion())) final var migrationContext = new MigrationContextImpl( previousStates, newStates, - nodeConfiguration, + appConfig, + platformConfig, genesisNetworkInfo, entityIdStore, previousVersion, diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/RosterToKey.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/RosterToKey.java new file mode 100644 index 000000000000..99c7555d4342 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/RosterToKey.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; +/** + * An enum representing the key either active roster or candidate roster. + * This value will be to key active roster if it is genesis stage. + */ +public enum RosterToKey { + /** + * Key the active roster. This is true when we are keying roster on genesis stage. + */ + ACTIVE_ROSTER, + + /** + * Key the candidate roster. This is true when we are keying roster on non-genesis stage. + */ + CANDIDATE_ROSTER, + + /** + * Key none of the roster. This is true when we are not keying any roster. + */ + NONE +} 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 73652706d96f..b14cc9e58f84 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 @@ -21,6 +21,7 @@ import com.hedera.hapi.node.state.roster.Roster; import com.hedera.node.app.roster.RosterService; import com.hedera.node.app.services.ServiceMigrator; +import com.hedera.node.app.spi.metrics.StoreMetricsService; import com.hedera.node.app.spi.workflows.HandleContext; import com.hedera.node.app.tss.handlers.TssHandlers; import com.hedera.node.app.tss.stores.ReadableTssStoreImpl; @@ -71,6 +72,11 @@ default String getServiceName() { return NAME; } + @Override + default int migrationOrder() { + return MIGRATION_ORDER; + } + /** * Returns the status of the TSS service relative to the given roster, ledger id, and given TSS base state. * @@ -161,7 +167,7 @@ Roster chooseRosterForNetwork( * Generates the participant directory for the active roster. * @param state the network state */ - void generateParticipantDirectory(@NonNull State state); + void ensureParticipantDirectoryKnown(@NonNull State state); /** * Returns the ledger id from the given TSS participant directory and TSS messages. @@ -181,7 +187,18 @@ Roster chooseRosterForNetwork( /** * Manages and does work based on the TSS status. - * @param state the network state + * It is called each second and computes the TSS status, based on the network state. + * If the self-node has any pending TSS submissions that can help progress the TSS Status, then it will + * submit them. + * + * @param state the network state + * @param isStakePeriodBoundary whether the current consensus round is a stake period boundary + * @param consensusNow the current consensus time + * @param storeMetricsService the store metrics service */ - void manageTssStatus(State state); + void manageTssStatus( + final State state, + final boolean isStakePeriodBoundary, + final Instant consensusNow, + final StoreMetricsService storeMetricsService); } 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 index eef8c1743f12..f31d293f32c8 100644 --- 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 @@ -57,4 +57,6 @@ TssBaseServiceComponent create( TssKeysAccessor tssKeysAccessor(); TssDirectoryAccessor tssDirectoryAccessor(); + + TssCryptographyManager tssCryptographyManager(); } 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 index 05ad252c1b34..aa0d2292f699 100644 --- 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 @@ -17,32 +17,47 @@ package com.hedera.node.app.tss; import static com.hedera.node.app.hapi.utils.CommonUtils.noThrowSha384HashOf; +import static com.hedera.node.app.tss.RosterToKey.ACTIVE_ROSTER; +import static com.hedera.node.app.tss.RosterToKey.CANDIDATE_ROSTER; +import static com.hedera.node.app.tss.RosterToKey.NONE; import static com.hedera.node.app.tss.TssBaseService.Status.PENDING_LEDGER_ID; +import static com.hedera.node.app.tss.TssKeyingStatus.KEYING_COMPLETE; +import static com.hedera.node.app.tss.TssKeyingStatus.WAITING_FOR_ENCRYPTION_KEYS; +import static com.hedera.node.app.tss.TssKeyingStatus.WAITING_FOR_THRESHOLD_TSS_MESSAGES; +import static com.hedera.node.app.tss.TssKeyingStatus.WAITING_FOR_THRESHOLD_TSS_VOTES; +import static com.hedera.node.app.tss.handlers.TssUtils.SIGNATURE_SCHEMA; import static com.hedera.node.app.tss.handlers.TssUtils.computeParticipantDirectory; import static com.hedera.node.app.tss.handlers.TssUtils.hasMetThreshold; +import static com.hedera.node.app.tss.handlers.TssUtils.voteForValidMessages; import static com.swirlds.platform.roster.RosterRetriever.getCandidateRosterHash; import static com.swirlds.platform.roster.RosterRetriever.retrieveActiveOrGenesisRoster; import static com.swirlds.platform.system.InitTrigger.GENESIS; import static java.util.Objects.requireNonNull; import com.google.common.annotations.VisibleForTesting; +import com.hedera.cryptography.bls.BlsPublicKey; import com.hedera.cryptography.tss.api.TssMessage; import com.hedera.cryptography.tss.api.TssParticipantDirectory; import com.hedera.hapi.node.state.primitives.ProtoBytes; import com.hedera.hapi.node.state.roster.Roster; +import com.hedera.hapi.node.state.tss.TssEncryptionKeys; import com.hedera.hapi.node.state.tss.TssVoteMapKey; import com.hedera.hapi.services.auxiliary.tss.TssMessageTransactionBody; import com.hedera.hapi.services.auxiliary.tss.TssShareSignatureTransactionBody; +import com.hedera.hapi.services.auxiliary.tss.TssVoteTransactionBody; import com.hedera.node.app.roster.RosterService; +import com.hedera.node.app.roster.schemas.V0540RosterSchema; import com.hedera.node.app.services.ServiceMigrator; import com.hedera.node.app.spi.AppContext; +import com.hedera.node.app.spi.metrics.StoreMetricsService; import com.hedera.node.app.spi.workflows.HandleContext; import com.hedera.node.app.store.ReadableStoreFactory; +import com.hedera.node.app.tss.api.FakeGroupElement; import com.hedera.node.app.tss.api.TssLibrary; 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.schemas.V0570TssBaseSchema; +import com.hedera.node.app.tss.schemas.V0580TssBaseSchema; import com.hedera.node.app.tss.stores.ReadableTssStore; import com.hedera.node.app.tss.stores.ReadableTssStoreImpl; import com.hedera.node.app.version.ServicesSoftwareVersion; @@ -53,22 +68,26 @@ import com.swirlds.metrics.api.Metrics; import com.swirlds.platform.roster.RosterUtils; import com.swirlds.platform.state.service.ReadableRosterStore; -import com.swirlds.platform.state.service.schemas.V0540RosterSchema; import com.swirlds.platform.system.InitTrigger; import com.swirlds.state.State; import com.swirlds.state.lifecycle.SchemaRegistry; import com.swirlds.state.spi.ReadableKVState; import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.math.BigInteger; import java.time.Instant; import java.time.InstantSource; import java.util.LinkedHashMap; import java.util.List; +import java.util.Objects; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.function.LongFunction; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -93,6 +112,20 @@ public class TssBaseServiceImpl implements TssBaseService { private final TssKeysAccessor tssKeysAccessor; private final TssDirectoryAccessor tssDirectoryAccessor; private final AppContext appContext; + private final TssCryptographyManager tssCryptographyManager; + // Indicates whether the current node has already submitted a tss message for the target roster. + // This is false by default and will be set to true when the node submits a message for the target roster. + // This is reset to false when we start keying a candidate roster + private boolean haveSentMessageForTargetRoster; + // Indicates whether the current node has already submitted a tss vote for the target roster. + // This is false by default and will be set to true when the node submits a vote for the target roster. + // This is reset to false when we start keying a candidate roster + private boolean haveSentVoteForTargetRoster; + // Indicates the current TssStatus of the network. + // This is used to determine the next steps in the TSS lifecycle. + // This is set to null by default and will be updated from state when each second is processed. + // This is also null when the network restarts or reconnects. + private TssStatus tssStatus; public TssBaseServiceImpl( @NonNull final AppContext appContext, @@ -121,13 +154,14 @@ public TssBaseServiceImpl( this.tssHandlers = new TssHandlers( component.tssMessageHandler(), component.tssVoteHandler(), component.tssShareSignatureHandler()); this.tssSubmissions = component.tssSubmissions(); + this.tssCryptographyManager = component.tssCryptographyManager(); } @Override public void registerSchemas(@NonNull final SchemaRegistry registry) { requireNonNull(registry); registry.register(new V0560TssBaseSchema()); - registry.register(new V0570TssBaseSchema()); + registry.register(new V0580TssBaseSchema()); } @Override @@ -166,12 +200,14 @@ public void setCandidateRoster(@NonNull final Roster candidateRoster, @NonNull f final var maxSharesPerNode = context.configuration().getConfigData(TssConfig.class).maxSharesPerNode(); - final var selfId = (int) context.networkInfo().selfNodeInfo().nodeId(); - final var candidateDirectory = computeParticipantDirectory(candidateRoster, maxSharesPerNode); + // TODO - use the real encryption keys from state + final LongFunction encryptionKeyFn = + nodeId -> new BlsPublicKey(new FakeGroupElement(BigInteger.valueOf(nodeId)), SIGNATURE_SCHEMA); + final var candidateDirectory = computeParticipantDirectory(candidateRoster, maxSharesPerNode, encryptionKeyFn); final var activeRoster = requireNonNull( context.storeFactory().readableStore(ReadableRosterStore.class).getActiveRoster()); - final var activeRosterHash = RosterUtils.hash(activeRoster).getBytes(); + final var sourceRosterHash = RosterUtils.hash(activeRoster).getBytes(); final var tssPrivateShares = tssKeysAccessor.accessTssKeys().activeRosterShares(); @@ -183,7 +219,7 @@ public void setCandidateRoster(@NonNull final Roster candidateRoster, @NonNull f () -> { final var msg = tssLibrary.generateTssMessage(candidateDirectory, tssPrivateShare); final var tssMessage = TssMessageTransactionBody.newBuilder() - .sourceRosterHash(activeRosterHash) + .sourceRosterHash(sourceRosterHash) .targetRosterHash(candidateRosterHash) .shareIndex(shareIndex.getAndAdd(1)) .tssMessage(Bytes.wrap(msg.toBytes())) @@ -269,10 +305,10 @@ public TssHandlers tssHandlers() { @Override @NonNull public Roster chooseRosterForNetwork( - @NonNull State state, - @NonNull InitTrigger trigger, - @NonNull ServiceMigrator serviceMigrator, - @NonNull ServicesSoftwareVersion version, + @NonNull final State state, + @NonNull final InitTrigger trigger, + @NonNull final ServiceMigrator serviceMigrator, + @NonNull final ServicesSoftwareVersion version, @NonNull final Configuration configuration, @NonNull final Roster overrideRoster) { if (!configuration.getConfigData(TssConfig.class).keyCandidateRoster()) { @@ -304,7 +340,7 @@ public void regenerateKeyMaterial(@NonNull final State state) { } @Override - public void generateParticipantDirectory(@NonNull final State state) { + public void ensureParticipantDirectoryKnown(@NonNull final State state) { tssDirectoryAccessor.generateTssParticipantDirectory(state); } @@ -380,12 +416,364 @@ public TssMessage getTssMessageFromBytes(Bytes wrap, TssParticipantDirectory dir } @Override - public void manageTssStatus(final State state) { - // TODO: Implement this method + public void manageTssStatus( + final State state, + final boolean isStakePeriodBoundary, + final Instant consensusNow, + final StoreMetricsService storeMetricsService) { + if (!appContext.configSupplier().get().getConfigData(TssConfig.class).keyCandidateRoster()) { + return; + } + final var readableStoreFactory = new ReadableStoreFactory(state); + final var tssStore = readableStoreFactory.getStore(ReadableTssStore.class); + final var rosterStore = readableStoreFactory.getStore(ReadableRosterStore.class); + // If the Tss Status is not computed yet during restart or reconnect, compute it from state. + if (tssStatus == null) { + tssStatus = computeInitialTssStatus(tssStore, rosterStore); + } + + // In order for the TSS state machine to run asynchronously in a separate thread, all the necessary + // information is collected and passed to the manageTssStatus method. + final var targetRosterHash = getTargetRosterHash( + requireNonNull(rosterStore.getActiveRoster()), rosterStore.getCandidateRoster(), tssStatus); + + // collect tss encryption keys for all nodes in the active roster that are not null + final var targetRoster = rosterStore.get(targetRosterHash); + final List targetRosterEncryptionKeys = targetRoster == null + ? List.of() + : targetRoster.rosterEntries().stream() + .map(entry -> tssStore.getTssEncryptionKeys(entry.nodeId())) + .filter(Objects::nonNull) + .filter(k -> k.currentEncryptionKey().equals(Bytes.EMPTY)) + .toList(); + + final var voteKey = new TssVoteMapKey( + targetRosterHash, appContext.selfNodeInfoSupplier().get().nodeId()); + final var info = new RosterAndTssInfo( + rosterStore.getActiveRoster(), + requireNonNull(rosterStore.getCurrentRosterHash()), + rosterStore.getCandidateRoster(), + targetRosterHash, + tssStore.getMessagesForTarget(targetRosterHash), + tssStore.anyWinningVoteFrom(rosterStore.getCurrentRosterHash(), targetRosterHash, rosterStore), + targetRosterEncryptionKeys, + tssStore.getVote(voteKey)); + CompletableFuture.runAsync( + () -> updateTssStatus(isStakePeriodBoundary, consensusNow, info), tssLibraryExecutor); + } + + /** + * Computes the initial TssStatus when the network restarts or reconnects or on genesis. + * This is called only once when JVM restarts. + * + * @param tssStore the TSS store + * @param rosterStore the roster store + * @return the initial TssStatus + */ + TssStatus computeInitialTssStatus(final ReadableTssStore tssStore, final ReadableRosterStore rosterStore) { + final var activeRosterHash = requireNonNull(rosterStore.getCurrentRosterHash()); + final var candidateRoster = rosterStore.getCandidateRoster(); + final var candidateRosterHash = + candidateRoster != null ? RosterUtils.hash(candidateRoster).getBytes() : null; + + final var winningVoteActive = tssStore.anyWinningVoteFor(activeRosterHash, rosterStore); + if (winningVoteActive.isEmpty()) { + final var keyingStatus = getTssKeyingStatus(tssStore, activeRosterHash, rosterStore.getActiveRoster()); + return new TssStatus(keyingStatus, ACTIVE_ROSTER, Bytes.EMPTY); + } + + final var activeRosterLedgerId = winningVoteActive.get().ledgerId(); + if (candidateRosterHash != null) { + final var winningVoteCandidate = + tssStore.anyWinningVoteFrom(activeRosterHash, candidateRosterHash, rosterStore); + return winningVoteCandidate + .map(voteBody -> new TssStatus(KEYING_COMPLETE, NONE, voteBody.ledgerId())) + .orElseGet(() -> { + final var keyingStatus = + getTssKeyingStatus(tssStore, candidateRosterHash, rosterStore.getCandidateRoster()); + return new TssStatus(keyingStatus, CANDIDATE_ROSTER, activeRosterLedgerId); + }); + } + + return new TssStatus(KEYING_COMPLETE, NONE, activeRosterLedgerId); + } + + /** + * Verifies the current TSS status when computing initial status. + * + * @param tssStore the TSS store + * @param targetRosterHash the target roster hash + * @param targetRoster the target roster + * @return the TSS keying status + */ + private TssKeyingStatus getTssKeyingStatus( + final ReadableTssStore tssStore, final Bytes targetRosterHash, final Roster targetRoster) { + final var numEncryptionKeys = requireNonNull(targetRoster).rosterEntries().stream() + .map(entry -> tssStore.getTssEncryptionKeys(entry.nodeId())) + .filter(Objects::nonNull) + .filter(k -> !k.currentEncryptionKey().equals(Bytes.EMPTY)) + .count(); + if (numEncryptionKeys != targetRoster.rosterEntries().size()) { + return WAITING_FOR_ENCRYPTION_KEYS; + } + // Since this is called only once when JVM restarts, it is okay to do these synchronously. + final var activeDirectory = tssDirectoryAccessor.activeParticipantDirectory(); + final var tssMessages = tssStore.getMessagesForTarget(targetRosterHash); + final var result = voteForValidMessages(tssMessages, activeDirectory, tssLibrary); + if (result.isEmpty()) { + return WAITING_FOR_THRESHOLD_TSS_MESSAGES; + } else { + return WAITING_FOR_THRESHOLD_TSS_VOTES; + } + } + + /** + * Computes the next TSS status from the state. + * + * @param isStakePeriodBoundary whether the current consensus round is a stake period boundary + * @param consensusNow the current consensus time + * @param info the roster and TSS information + */ + void updateTssStatus(final boolean isStakePeriodBoundary, final Instant consensusNow, final RosterAndTssInfo info) { + final var statusChange = new StatusChange(isStakePeriodBoundary, consensusNow, info); + this.tssStatus = statusChange.computeNewStatus(); } @VisibleForTesting public TssKeysAccessor getTssKeysAccessor() { return tssKeysAccessor; } + + /** + * A class to manage the status change of the TSS. + * It computes the new status based on the old status and the current state of the system. + * If needed, it schedules work to generate TSS messages and votes. + */ + public class StatusChange { + private TssKeyingStatus newKeyingStatus; + private RosterToKey newRosterToKey; + private Bytes newLedgerId; + private final boolean isStakePeriodBoundary; + private final Instant consensusNow; + private final RosterAndTssInfo info; + + public StatusChange( + final boolean isStakePeriodBoundary, final Instant consensusNow, final RosterAndTssInfo info) { + this.isStakePeriodBoundary = isStakePeriodBoundary; + this.info = info; + this.newKeyingStatus = tssStatus.tssKeyingStatus(); + this.newRosterToKey = tssStatus.rosterToKey(); + this.newLedgerId = tssStatus.ledgerId(); + this.consensusNow = consensusNow; + } + + /** + * Computes the new status based on the old status and the current state of the system. + * If needed, it schedules work to generate TSS messages and votes. + * + * @return the new status + */ + public TssStatus computeNewStatus() { + switch (tssStatus.rosterToKey()) { + case NONE -> { + if (isStakePeriodBoundary) { + newRosterToKey = CANDIDATE_ROSTER; + newKeyingStatus = WAITING_FOR_ENCRYPTION_KEYS; + haveSentMessageForTargetRoster = false; + haveSentVoteForTargetRoster = false; + } + } + case CANDIDATE_ROSTER -> { + final var activeRosterHash = requireNonNull(info.activeRosterHash()); + final var candidateRosterHash = RosterUtils.hash(requireNonNull(info.candidateRoster())) + .getBytes(); + + switch (tssStatus.tssKeyingStatus()) { + case KEYING_COMPLETE -> newRosterToKey = NONE; + case WAITING_FOR_THRESHOLD_TSS_MESSAGES -> validateMessagesAndSubmitIfNeeded( + activeRosterHash, candidateRosterHash); + case WAITING_FOR_THRESHOLD_TSS_VOTES -> validateVotesAndSubmitIfNeeded( + activeRosterHash, candidateRosterHash); + case WAITING_FOR_ENCRYPTION_KEYS -> validateThresholdEncryptionKeysReached( + info.candidateRoster()); + } + } + case ACTIVE_ROSTER -> { + requireNonNull(info.activeRosterHash()); + switch (tssStatus.tssKeyingStatus()) { + case KEYING_COMPLETE -> newRosterToKey = NONE; + case WAITING_FOR_THRESHOLD_TSS_MESSAGES -> validateMessagesAndSubmitIfNeeded( + Bytes.EMPTY, info.activeRosterHash()); + case WAITING_FOR_THRESHOLD_TSS_VOTES -> validateVotesAndSubmitIfNeeded( + Bytes.EMPTY, info.activeRosterHash()); + case WAITING_FOR_ENCRYPTION_KEYS -> validateThresholdEncryptionKeysReached(info.activeRoster()); + } + } + } + return new TssStatus(newKeyingStatus, newRosterToKey, newLedgerId); + } + + /** + * Validates the votes and submits a vote for the current node if needed to reach the threshold. + * + * @param targetRosterHash the target roster hash + * @param sourceRosterHash the source roster hash + */ + private void validateVotesAndSubmitIfNeeded(final Bytes sourceRosterHash, final Bytes targetRosterHash) { + final var voteBodies = info.winningVote(); + if (voteBodies.isPresent()) { + newKeyingStatus = KEYING_COMPLETE; + newLedgerId = voteBodies.get().ledgerId(); + } else if (!haveSentVoteForTargetRoster && info.selfVote() == null) { + // Obtain the directory of participants for the source roster + final var directory = tssDirectoryAccessor.activeParticipantDirectory(); + final var vote = tssCryptographyManager.getVote(info.tssMessages(), directory); + if (vote != null) { + final var tssVote = TssVoteTransactionBody.newBuilder() + .tssVote(vote.bitSet()) + .sourceRosterHash(sourceRosterHash) + .targetRosterHash(targetRosterHash) + .ledgerId(vote.ledgerId()) + .nodeSignature(vote.signature().getBytes()) + .build(); + tssSubmissions.submitTssVote(tssVote, consensusNow); + haveSentVoteForTargetRoster = true; + } + } + } + + /** + * Validates the messages and submits a message for the current node if needed to reach the threshold. + * + * @param sourceRosterHash the source roster hash + * @param targetRosterHash the target roster hash + */ + private void validateMessagesAndSubmitIfNeeded(final Bytes sourceRosterHash, final Bytes targetRosterHash) { + final var thresholdReached = validateThresholdTssMessages(); + if (thresholdReached) { + newKeyingStatus = WAITING_FOR_THRESHOLD_TSS_VOTES; + } else if (!haveSentMessageForTargetRoster) { + if (tssStatus.rosterToKey() == ACTIVE_ROSTER) { + final var msg = tssLibrary.generateTssMessage(tssDirectoryAccessor.activeParticipantDirectory()); + final var tssMessage = TssMessageTransactionBody.newBuilder() + .sourceRosterHash(sourceRosterHash) + .targetRosterHash(targetRosterHash) + .shareIndex(appContext.selfNodeInfoSupplier().get().nodeId() + 1) + .tssMessage(Bytes.wrap(msg.toBytes())) + .build(); + // need to use consensusNow here + tssSubmissions.submitTssMessage(tssMessage, consensusNow); + haveSentMessageForTargetRoster = true; + } else if (tssStatus.rosterToKey() == CANDIDATE_ROSTER) { + // Obtain the directory of participants for the target roster + // submit ours and set haveSentMessageForTargetRoster to true + final var tssPrivateShares = tssKeysAccessor.accessTssKeys().activeRosterShares(); + for (final var tssPrivateShare : tssPrivateShares) { + final var msg = tssLibrary.generateTssMessage( + tssDirectoryAccessor.generateTssParticipantDirectoryFor(info.candidateRoster()), + tssPrivateShare); + final var tssMessage = TssMessageTransactionBody.newBuilder() + .sourceRosterHash(sourceRosterHash) + .targetRosterHash(targetRosterHash) + .shareIndex(tssPrivateShare.shareId()) + .tssMessage(Bytes.wrap(msg.toBytes())) + .build(); + // need to use consensusNow here + tssSubmissions.submitTssMessage(tssMessage, consensusNow); + haveSentMessageForTargetRoster = true; + } + } + } + } + + /** + * Validates the threshold of TSS messages. + * + * @return true if the threshold is met, false otherwise + */ + private boolean validateThresholdTssMessages() { + final var participantDirectory = tssDirectoryAccessor.activeParticipantDirectory(); + final var tssMessageBodies = info.tssMessages(); + return voteForValidMessages(tssMessageBodies, participantDirectory, tssLibrary) + .isPresent(); + } + + /** + * Validates the threshold of encryption keys and creates the encryption key for self if not present. + */ + private void validateThresholdEncryptionKeysReached(final Roster roster) { + var numTssEncryptionKeys = info.targetRosterEncryptionKeys().size(); + final var thresholdReached = numTssEncryptionKeys + >= (2 * requireNonNull(roster).rosterEntries().size()) / 3; + if (thresholdReached) { + newKeyingStatus = TssKeyingStatus.WAITING_FOR_THRESHOLD_TSS_MESSAGES; + } else { + // TODO: Create the encryption key for self if not present + } + } + } + + /** + * A record to hold the roster and TSS information that is needed to compute new TSS status. + * + * @param activeRoster the active roster + * @param activeRosterHash the active roster hash + * @param candidateRoster the candidate roster + * @param targetRosterHash the target roster hash + * @param tssMessages the TSS messages for the target roster + * @param winningVote the winning vote for the target roster + * @param targetRosterEncryptionKeys the encryption keys for the active roster + * @param selfVote the self vote for the current node + */ + public record RosterAndTssInfo( + @NonNull Roster activeRoster, + @NonNull Bytes activeRosterHash, + @Nullable Roster candidateRoster, + @NonNull Bytes targetRosterHash, + @NonNull List tssMessages, + @NonNull Optional winningVote, + @NonNull List targetRosterEncryptionKeys, + TssVoteTransactionBody selfVote) {} + + /** + * Returns the target roster hash based on the current TSS status roster to key. + * + * @param sourceRoster the active roster + * @param candidateRoster the candidate roster + * @param tssStatus the TSS status + * @return the target roster hash + */ + @NonNull + private Bytes getTargetRosterHash( + @NonNull final Roster sourceRoster, + @Nullable final Roster candidateRoster, + @NonNull final TssStatus tssStatus) { + final var rosterToKey = tssStatus.rosterToKey(); + return switch (rosterToKey) { + case ACTIVE_ROSTER -> RosterUtils.hash(requireNonNull(sourceRoster)).getBytes(); + case CANDIDATE_ROSTER -> RosterUtils.hash(requireNonNull(candidateRoster)) + .getBytes(); + case NONE -> Bytes.EMPTY; + }; + } + + @VisibleForTesting + public TssStatus getTssStatus() { + return tssStatus; + } + + @VisibleForTesting + public void setTssStatus(final TssStatus tssStatus) { + this.tssStatus = tssStatus; + } + + @VisibleForTesting + public boolean haveSentVoteForTargetRoster() { + return haveSentVoteForTargetRoster; + } + + @VisibleForTesting + public boolean haveSentMessageForTargetRoster() { + return haveSentMessageForTargetRoster; + } } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/TssCryptographyManager.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/TssCryptographyManager.java index 0e644cc05460..122d0c89c096 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/TssCryptographyManager.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/TssCryptographyManager.java @@ -16,24 +16,21 @@ package com.hedera.node.app.tss; -import static com.hedera.node.app.tss.handlers.TssUtils.getThresholdForTssMessages; import static com.hedera.node.app.tss.handlers.TssUtils.getTssMessages; -import static com.hedera.node.app.tss.handlers.TssUtils.validateTssMessages; +import static com.hedera.node.app.tss.handlers.TssUtils.voteForValidMessages; import static java.util.Objects.requireNonNull; import com.hedera.cryptography.bls.BlsPublicKey; import com.hedera.cryptography.tss.api.TssMessage; import com.hedera.cryptography.tss.api.TssParticipantDirectory; -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.hedera.node.app.spi.AppContext; -import com.hedera.node.app.spi.workflows.HandleContext; import com.hedera.node.app.tss.api.TssLibrary; -import com.hedera.node.app.tss.stores.WritableTssStore; import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.common.crypto.Signature; import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; import java.time.Duration; import java.time.InstantSource; import java.util.BitSet; @@ -91,20 +88,16 @@ public record Vote( * given hash, based on incorporating all available {@link TssMessage}s, if * the threshold number of messages are available. The signature is with the node's RSA key used for gossip. * - * @param targetRosterHash the hash of the target roster - * @param directory the TSS participant directory - * @param context the handle context to use in setting up the computation + * @param directory the TSS participant directory + * @param tssMessageBodies the list of TSS message bodies + * @param voteBody the vote body * @return a future resolving to the signed vote if given message passes the threshold, or null otherwise */ public CompletableFuture getVoteFuture( - @NonNull final Bytes targetRosterHash, @NonNull final TssParticipantDirectory directory, - @NonNull final HandleContext context) { - final var tssStore = context.storeFactory().writableStore(WritableTssStore.class); - final var tssMessageBodies = tssStore.getMessagesForTarget(targetRosterHash); - final var voteKey = new TssVoteMapKey( - targetRosterHash, context.networkInfo().selfNodeInfo().nodeId()); - if (tssStore.getVote(voteKey) == null) { + @NonNull final List tssMessageBodies, + @Nullable final TssVoteTransactionBody voteBody) { + if (voteBody == null) { return computeVote(tssMessageBodies, directory).exceptionally(e -> { log.error("Error computing public keys and signing", e); return null; @@ -124,56 +117,27 @@ public CompletableFuture getVoteFuture( private CompletableFuture computeVote( @NonNull final List tssMessageBodies, @NonNull final TssParticipantDirectory tssParticipantDirectory) { - return CompletableFuture.supplyAsync( - () -> { - final var tssMessages = validateTssMessages(tssMessageBodies, tssParticipantDirectory, tssLibrary); - if (!isThresholdMet(tssMessages, tssParticipantDirectory)) { - return null; - } - final var aggregationStart = instantSource.instant(); - final var validTssMessages = getTssMessages(tssMessages, tssParticipantDirectory, tssLibrary); - final var publicShares = tssLibrary.computePublicShares(tssParticipantDirectory, validTssMessages); - final var ledgerId = tssLibrary.aggregatePublicShares(publicShares); - final var signature = gossip.sign(ledgerId.toBytes()); - final var thresholdMessages = asBitSet(tssMessages); - final var aggregationEnd = instantSource.instant(); - tssMetrics.updateAggregationTime( - Duration.between(aggregationStart, aggregationEnd).toMillis()); - return new Vote(ledgerId, signature, thresholdMessages); - }, - libraryExecutor); + return CompletableFuture.supplyAsync(() -> getVote(tssMessageBodies, tssParticipantDirectory), libraryExecutor); } - /** - * Compute the TSS vote bit set. No need to validate the TSS messages here as they have already been validated. - * - * @param thresholdMessages the valid TSS messages - * @return the TSS vote bit set - */ - private BitSet asBitSet(@NonNull final List thresholdMessages) { - // TODO - fix this, nodes vote for TSS messages based on their position - // in consensus order of messages received for a roster hash, NOT by - // the message's share index - final var tssVoteBitSet = new BitSet(); - for (TssMessageTransactionBody op : thresholdMessages) { - tssVoteBitSet.set((int) op.shareIndex()); + @Nullable + public Vote getVote( + final @NonNull List tssMessageBodies, + final @NonNull TssParticipantDirectory tssParticipantDirectory) { + final var result = voteForValidMessages(tssMessageBodies, tssParticipantDirectory, tssLibrary); + if (result.isEmpty()) { + return null; } - return tssVoteBitSet; - } - - /** - * Check if the threshold consensus weight is met to submit a {@link TssVoteTransactionBody}. - * The threshold is met if more than half the consensus weight has been received. - * - * @param validTssMessages the valid TSS messages - * @param tssParticipantDirectory the TSS participant directory - * @return true if the threshold is met, false otherwise - */ - private boolean isThresholdMet( - @NonNull final List validTssMessages, - @NonNull final TssParticipantDirectory tssParticipantDirectory) { - final var numShares = tssParticipantDirectory.getShareIds().size(); - // If more than 1/2 the consensus weight has been received, then the threshold is met - return validTssMessages.size() >= getThresholdForTssMessages(numShares); + final var aggregationStart = instantSource.instant(); + final var validTssMessages = + getTssMessages(result.get().validTssMessages(), tssParticipantDirectory, tssLibrary); + final var publicShares = tssLibrary.computePublicShares(tssParticipantDirectory, validTssMessages); + final var ledgerId = tssLibrary.aggregatePublicShares(publicShares); + final var signature = gossip.sign(ledgerId.toBytes()); + final var vote = result.get().vote(); + final var aggregationEnd = instantSource.instant(); + tssMetrics.updateAggregationTime( + Duration.between(aggregationStart, aggregationEnd).toMillis()); + return new Vote(ledgerId, signature, vote); } } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/TssDirectoryAccessor.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/TssDirectoryAccessor.java index b6062092544a..c4fca0e45523 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/TssDirectoryAccessor.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/TssDirectoryAccessor.java @@ -16,18 +16,23 @@ package com.hedera.node.app.tss; +import static com.hedera.node.app.tss.handlers.TssUtils.SIGNATURE_SCHEMA; import static com.hedera.node.app.tss.handlers.TssUtils.computeParticipantDirectory; import static java.util.Objects.requireNonNull; +import com.hedera.cryptography.bls.BlsPublicKey; import com.hedera.cryptography.tss.api.TssParticipantDirectory; +import com.hedera.hapi.node.state.roster.Roster; import com.hedera.node.app.spi.AppContext; import com.hedera.node.app.store.ReadableStoreFactory; +import com.hedera.node.app.tss.api.FakeGroupElement; import com.hedera.node.config.data.TssConfig; import com.swirlds.config.api.Configuration; import com.swirlds.platform.state.service.ReadableRosterStore; import com.swirlds.state.State; -import com.swirlds.state.lifecycle.info.NodeInfo; import edu.umd.cs.findbugs.annotations.NonNull; +import java.math.BigInteger; +import java.util.function.LongFunction; import java.util.function.Supplier; import javax.inject.Inject; import javax.inject.Singleton; @@ -37,34 +42,75 @@ */ @Singleton public class TssDirectoryAccessor { - private TssParticipantDirectory tssParticipantDirectory; private final Supplier configurationSupplier; - private final Supplier nodeInfoSupplier; + + /** + * Non-final because it is lazy-initialized once state is available. + */ + private TssParticipantDirectory tssParticipantDirectory; @Inject public TssDirectoryAccessor(@NonNull final AppContext appContext) { - this.configurationSupplier = appContext.configSupplier(); - this.nodeInfoSupplier = appContext.selfNodeInfoSupplier(); + this.configurationSupplier = requireNonNull(appContext).configSupplier(); } /** * Generates the participant directory for the active roster. - * - * @param state state + * @param state state */ public void generateTssParticipantDirectory(@NonNull final State state) { + final var readableStoreFactory = new ReadableStoreFactory(state); + final var rosterStore = readableStoreFactory.getStore(ReadableRosterStore.class); + // TODO - use the real encryption keys from state + final LongFunction encryptionKeyFn = + nodeId -> new BlsPublicKey(new FakeGroupElement(BigInteger.valueOf(nodeId)), SIGNATURE_SCHEMA); + activeParticipantDirectoryFrom(rosterStore, encryptionKeyFn); + } + + /** + * Returns the {@link TssParticipantDirectory} for the active roster in the given store. + * + * @param rosterStore the store from which to retrieve the active roster + * @param encryptionKeyFn the function to get the TSS encryption keys + * @return the {@link TssParticipantDirectory} for the active roster + */ + public TssParticipantDirectory activeParticipantDirectoryFrom( + @NonNull final ReadableRosterStore rosterStore, @NonNull final LongFunction encryptionKeyFn) { + // Since the active roster can only change when restarting the JVM, we only compute it once + // per instantiation of this singleton accessor if (tssParticipantDirectory != null) { - return; + return tssParticipantDirectory; } + final var activeRoster = requireNonNull(rosterStore.getActiveRoster()); final var maxSharesPerNode = configurationSupplier.get().getConfigData(TssConfig.class).maxSharesPerNode(); - final var readableStoreFactory = new ReadableStoreFactory(state); - final var rosterStore = readableStoreFactory.getStore(ReadableRosterStore.class); - final var activeRoster = requireNonNull(rosterStore.getActiveRoster()); - this.tssParticipantDirectory = computeParticipantDirectory(activeRoster, maxSharesPerNode); + tssParticipantDirectory = computeParticipantDirectory(activeRoster, maxSharesPerNode, encryptionKeyFn); + return tssParticipantDirectory; + } + + /** + * Generates the participant directory for the given roster. + * + * @param roster Roster to generate participant directory for + */ + public TssParticipantDirectory generateTssParticipantDirectoryFor(@NonNull final Roster roster) { + final LongFunction encryptionKeyFn = + nodeId -> new BlsPublicKey(new FakeGroupElement(BigInteger.valueOf(nodeId)), SIGNATURE_SCHEMA); + final var maxSharesPerNode = + configurationSupplier.get().getConfigData(TssConfig.class).maxSharesPerNode(); + return computeParticipantDirectory(roster, maxSharesPerNode, encryptionKeyFn); } public TssParticipantDirectory activeParticipantDirectory() { return tssParticipantDirectory; } + + /** + * Returns the {@link TssParticipantDirectory} for the active roster. + * @return the {@link TssParticipantDirectory} for the active roster + * @throws NullPointerException if the participant directory has not been generated + */ + public @NonNull TssParticipantDirectory activeParticipantDirectoryOrThrow() { + return requireNonNull(tssParticipantDirectory); + } } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/TssKeyingStatus.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/TssKeyingStatus.java new file mode 100644 index 000000000000..6379ab07537b --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/TssKeyingStatus.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.tss; +/** + * An enum representing the status of the TSS keying process. + * This status SHALL be used to determine the state of the TSS keying process. + */ +public enum TssKeyingStatus { + + /** + * The TSS keying process has not yet reached the threshold for encryption + * keys. + */ + WAITING_FOR_ENCRYPTION_KEYS, + + /** + * The TSS keying process has not yet reached the threshold for TSS messages. + */ + WAITING_FOR_THRESHOLD_TSS_MESSAGES, + + /** + * The TSS keying process has not yet reached the threshold for TSS votes. + */ + WAITING_FOR_THRESHOLD_TSS_VOTES, + + /** + * The TSS keying process has completed and the ledger id is set. + */ + KEYING_COMPLETE +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/TssKeysAccessor.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/TssKeysAccessor.java index 762ea385a59f..1e6fb02d2d44 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/TssKeysAccessor.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/TssKeysAccessor.java @@ -17,7 +17,7 @@ package com.hedera.node.app.tss; import static com.hedera.node.app.tss.handlers.TssUtils.getTssMessages; -import static com.hedera.node.app.tss.handlers.TssUtils.validateTssMessages; +import static com.hedera.node.app.tss.handlers.TssUtils.getValidMessages; import static java.util.Objects.requireNonNull; import com.google.common.annotations.VisibleForTesting; @@ -64,7 +64,7 @@ public void generateKeyMaterialForActiveRoster(@NonNull final State state) { final var tssStore = storeFactory.getStore(ReadableTssStore.class); final var rosterStore = storeFactory.getStore(ReadableRosterStore.class); final var activeRosterHash = requireNonNull(rosterStore.getCurrentRosterHash()); - final var activeParticipantDirectory = tssDirectoryAccessor.activeParticipantDirectory(); + final var activeParticipantDirectory = tssDirectoryAccessor.activeParticipantDirectoryOrThrow(); final var tssMessageBodies = tssStore.getMessagesForTarget(activeRosterHash); final var validTssMessages = getTssMessages(tssMessageBodies, activeParticipantDirectory, tssLibrary); final var activeRosterShares = getTssPrivateShares(activeParticipantDirectory, tssStore, activeRosterHash); @@ -84,9 +84,9 @@ private List getTssPrivateShares( @NonNull final TssParticipantDirectory activeRosterParticipantDirectory, @NonNull final ReadableTssStore tssStore, @NonNull final Bytes activeRosterHash) { - final var validTssOps = validateTssMessages( + final var tssMessages = getValidMessages( tssStore.getMessagesForTarget(activeRosterHash), activeRosterParticipantDirectory, tssLibrary); - final var validTssMessages = getTssMessages(validTssOps, activeRosterParticipantDirectory, tssLibrary); + final var validTssMessages = getTssMessages(tssMessages, activeRosterParticipantDirectory, tssLibrary); return tssLibrary.decryptPrivateShares(activeRosterParticipantDirectory, validTssMessages); } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/TssStatus.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/TssStatus.java new file mode 100644 index 000000000000..25e6aa686f06 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/TssStatus.java @@ -0,0 +1,26 @@ +/* + * 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.pbj.runtime.io.buffer.Bytes; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * A Singleton state object that represents the status of the TSS keying process. + * This key SHALL be used to determine the stage of the TSS keying process. + */ +public record TssStatus(TssKeyingStatus tssKeyingStatus, RosterToKey rosterToKey, @NonNull Bytes ledgerId) {} 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 index dc434daef636..09e13b84c340 100644 --- 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 @@ -19,6 +19,7 @@ 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.node.transaction.TransactionBody; import com.hedera.hapi.services.auxiliary.tss.TssMessageTransactionBody; import com.hedera.hapi.services.auxiliary.tss.TssVoteTransactionBody; @@ -83,11 +84,15 @@ public void handle(@NonNull final HandleContext context) throws HandleException tssStore.put(key, op); // Obtain the directory of participants for the target roster - final var directory = tssDirectoryAccessor.activeParticipantDirectory(); + final var directory = tssDirectoryAccessor.activeParticipantDirectoryOrThrow(); // Schedule work to potentially compute a signed vote for the new key material of the target // roster, if this message was valid and passed the threshold number of messages required + final var selfNodeId = context.networkInfo().selfNodeInfo().nodeId(); + final var tssMessageBodies = tssStore.getMessagesForTarget(targetRosterHash); + final var voteKey = new TssVoteMapKey(targetRosterHash, selfNodeId); + final var voteBody = tssStore.getVote(voteKey); tssCryptographyManager - .getVoteFuture(op.targetRosterHash(), directory, context) + .getVoteFuture(directory, tssMessageBodies, voteBody) .thenAccept(vote -> { if (vote != null) { // FUTURE: Validate the ledgerId computed is same as the current ledgerId 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 index 79b821a4525a..926d67bdf23d 100644 --- 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 @@ -81,43 +81,67 @@ public TssSubmissions(@NonNull final AppContext appContext, @NonNull final Execu /** * Attempts to submit a TSS message to the network. * - * @param body the TSS message to submit + * @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) { + return submitTssMessage(body, nextValidStartFor(context)); + } + + /** + * Attempts to submit a TSS message to the network. + * + * @param body the TSS message to submit + * @param lastUsedConsensusTime the TSS context + * @return a future that completes when the message has been submitted + */ + public CompletableFuture submitTssMessage( + @NonNull final TssMessageTransactionBody body, @NonNull final Instant lastUsedConsensusTime) { requireNonNull(body); - requireNonNull(context); + requireNonNull(lastUsedConsensusTime); return submit( b -> b.tssMessage(body), - context.configuration(), - context.networkInfo().selfNodeInfo().accountId(), - nextValidStartFor(context)); + appContext.configSupplier().get(), + appContext.selfNodeInfoSupplier().get().accountId(), + lastUsedConsensusTime); } /** * Attempts to submit a TSS vote to the network. * - * @param body the TSS vote to submit - * @param context the TSS context + * @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) { + @NonNull final TssVoteTransactionBody body, final HandleContext context) { + return submitTssVote(body, nextValidStartFor(context)); + } + + /** + * Attempts to submit a TSS vote to the network. + * + * @param body the TSS vote to submit + * @param lastUsedConsensusTime the + * @return a future that completes when the vote has been submitted + */ + public CompletableFuture submitTssVote( + @NonNull final TssVoteTransactionBody body, final Instant lastUsedConsensusTime) { requireNonNull(body); - requireNonNull(context); + requireNonNull(lastUsedConsensusTime); return submit( b -> b.tssVote(body), - context.configuration(), - context.networkInfo().selfNodeInfo().accountId(), - nextValidStartFor(context)); + appContext.configSupplier().get(), + appContext.selfNodeInfoSupplier().get().accountId(), + lastUsedConsensusTime); } /** * Attempts to submit a TSS share signature to the network. * - * @param body the TSS share signature to submit + * @param body the TSS share signature to submit * @param lastUsedConsensusTime the last used consensus time * @return a future that completes when the share signature has been submitted */ diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/handlers/TssUtils.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/handlers/TssUtils.java index 745be12b0be6..94211209b0a0 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/handlers/TssUtils.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/handlers/TssUtils.java @@ -17,6 +17,7 @@ package com.hedera.node.app.tss.handlers; import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.toMap; import com.hedera.cryptography.bls.BlsPublicKey; import com.hedera.cryptography.bls.GroupAssignment; @@ -27,47 +28,72 @@ import com.hedera.hapi.node.state.roster.Roster; import com.hedera.hapi.node.state.roster.RosterEntry; import com.hedera.hapi.services.auxiliary.tss.TssMessageTransactionBody; +import com.hedera.hapi.services.auxiliary.tss.TssVoteTransactionBody; import com.hedera.node.app.tss.api.FakeGroupElement; import com.hedera.node.app.tss.api.TssLibrary; +import com.hedera.node.internal.network.Network; import edu.umd.cs.findbugs.annotations.NonNull; import java.math.BigInteger; +import java.util.BitSet; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.function.LongFunction; public class TssUtils { public static final SignatureSchema SIGNATURE_SCHEMA = SignatureSchema.create(Curve.ALT_BN128, GroupAssignment.SHORT_SIGNATURES); + + /** + * Given a network, return a function that maps node IDs to their TSS encryption keys in the network (if + * this information is available). + * @param network the network + * @return a function that maps node IDs to their TSS encryption keys + */ + public static LongFunction encryptionKeysFnFor(@NonNull final Network network) { + return network.nodeMetadata().stream() + .filter(metadata -> metadata.tssEncryptionKey().length() > 0) + .collect(toMap( + metadata -> metadata.nodeOrThrow().nodeId(), + // TODO - compute the real public key + metadata -> new BlsPublicKey( + new FakeGroupElement(new BigInteger( + metadata.tssEncryptionKey().toByteArray())), + SIGNATURE_SCHEMA)))::get; + } /** * Compute the TSS participant directory from the roster. * - * @param roster the roster + * @param roster the roster * @param maxSharesPerNode the maximum number of shares per node + * @param tssEncryptionKeyFn the function to get the TSS encryption keys * @return the TSS participant directory */ public static TssParticipantDirectory computeParticipantDirectory( - @NonNull final Roster roster, final long maxSharesPerNode) { + @NonNull final Roster roster, + final int maxSharesPerNode, + @NonNull final LongFunction tssEncryptionKeyFn) { final var computedShares = computeNodeShares(roster.rosterEntries(), maxSharesPerNode); final var totalShares = computedShares.values().stream().mapToLong(Long::longValue).sum(); final var threshold = getThresholdForTssMessages(totalShares); final var builder = TssParticipantDirectory.createBuilder().withThreshold(threshold); - for (var rosterEntry : roster.rosterEntries()) { + for (final var rosterEntry : roster.rosterEntries()) { final int numSharesPerThisNode = computedShares.get(rosterEntry.nodeId()).intValue(); - // FUTURE: Use the actual public key from the node - final var pairingPublicKey = - new BlsPublicKey(new FakeGroupElement(BigInteger.valueOf(10L)), SIGNATURE_SCHEMA); - builder.withParticipant(rosterEntry.nodeId(), numSharesPerThisNode, pairingPublicKey); + final long nodeId = rosterEntry.nodeId(); + final var encryptionKey = + requireNonNull(tssEncryptionKeyFn.apply(nodeId), "No encryption key for node" + nodeId); + builder.withParticipant(nodeId, numSharesPerThisNode, encryptionKey); } - // FUTURE: Use the actual signature schema return builder.build(); } /** - * Compute the threshold of consensus weight needed for submitting a {@link com.hedera.hapi.services.auxiliary.tss.TssVoteTransactionBody} + * Compute the threshold of consensus weight needed for submitting a {@link TssVoteTransactionBody} * If more than 1/2 the consensus weight has been received, then the threshold is met * * @param totalShares the total number of shares @@ -77,6 +103,38 @@ public static int getThresholdForTssMessages(final long totalShares) { return (int) (totalShares + 2) / 2; } + /** + * Validate TSS messages using the TSS library. If the message is valid, add it to the list of valid TSS messages. + * If the threshold is met, return the list of valid TSS messages and the vote bit set. + * + * @param tssMessages list of TSS messages to validate + * @param tssParticipantDirectory the participant directory + * @return list of valid TSS messages + */ + public static Optional voteForValidMessages( + @NonNull final List tssMessages, + @NonNull final TssParticipantDirectory tssParticipantDirectory, + @NonNull final TssLibrary tssLibrary) { + final var threshold = + getThresholdForTssMessages(tssParticipantDirectory.getShareIds().size()); + int numValidMessages = 0; + final var validTssMessages = new LinkedList(); + final var bitSet = new BitSet(); + for (int i = 0; i < tssMessages.size(); i++) { + final var isValid = tssLibrary.verifyTssMessage( + tssParticipantDirectory, tssMessages.get(i).tssMessage()); + if (isValid) { + bitSet.set(i); + validTssMessages.add(tssMessages.get(i)); + numValidMessages++; + } + if (numValidMessages >= threshold) { + return Optional.of(new ValidMessagesWithVote(validTssMessages, bitSet)); + } + } + return Optional.empty(); + } + /** * Validate TSS messages using the TSS library. If the message is valid, add it to the list of valid TSS messages. * @@ -84,15 +142,18 @@ public static int getThresholdForTssMessages(final long totalShares) { * @param tssParticipantDirectory the participant directory * @return list of valid TSS messages */ - public static List validateTssMessages( + public static List getValidMessages( @NonNull final List tssMessages, @NonNull final TssParticipantDirectory tssParticipantDirectory, @NonNull final TssLibrary tssLibrary) { final var validTssMessages = new LinkedList(); - for (final var op : tssMessages) { - final var isValid = tssLibrary.verifyTssMessage(tssParticipantDirectory, op.tssMessage()); + final var bitSet = new BitSet(); + for (int i = 0; i < tssMessages.size(); i++) { + final var isValid = tssLibrary.verifyTssMessage( + tssParticipantDirectory, tssMessages.get(i).tssMessage()); if (isValid) { - validTssMessages.add(op); + bitSet.set(i); + validTssMessages.add(tssMessages.get(i)); } } return validTssMessages; @@ -132,7 +193,8 @@ public static Map computeNodeShares( /** * Compute the number of shares each node should have based on the weight of the node. - * @param weights the map of node ID to weight + * + * @param weights the map of node ID to weight * @param maxShares the maximum number of shares * @return a map of node ID to the number of shares */ @@ -152,11 +214,14 @@ public static Map computeSharesFromWeights( /** * Returns whether a vote bitset with the given weight has met the threshold for a roster with the given * total weight. - * @param voteWeight the weight of the vote bitset + * + * @param voteWeight the weight of the vote bitset * @param totalWeight the total weight of the roster * @return true if the threshold has been met, false otherwise */ public static boolean hasMetThreshold(final long voteWeight, final long totalWeight) { return voteWeight >= (totalWeight + 2) / 3; } + + public record ValidMessagesWithVote(List validTssMessages, BitSet vote) {} } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/schemas/TssBaseTransplantSchema.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/schemas/TssBaseTransplantSchema.java new file mode 100644 index 000000000000..5e6e358df67a --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/schemas/TssBaseTransplantSchema.java @@ -0,0 +1,122 @@ +/* + * 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.schemas; + +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 com.hedera.node.app.tss.schemas.V0580TssBaseSchema.TSS_ENCRYPTION_KEYS_KEY; +import static java.util.Objects.requireNonNull; + +import com.hedera.hapi.node.state.common.EntityNumber; +import com.hedera.hapi.node.state.tss.TssEncryptionKeys; +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.hedera.node.app.tss.TssBaseService; +import com.hedera.node.config.data.TssConfig; +import com.hedera.node.internal.network.Network; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.swirlds.platform.roster.RosterUtils; +import com.swirlds.state.lifecycle.MigrationContext; +import com.swirlds.state.lifecycle.Schema; +import com.swirlds.state.spi.WritableKVState; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.BitSet; +import java.util.concurrent.atomic.AtomicLong; + +/** + * The {@link Schema#restart(MigrationContext)} implementation whereby the {@link TssBaseService} ensures that any + * TSS keys in the startup assets are copied into the state. These assets may range from no keys at all, to just + * encryption keys; to a full set of TSS messages with shares for a transplant roster encrypted using the encryption + * keys in the override address book. + *

    + * Important: The latest {@link TssBaseService} schema should always implement this interface. + */ +public interface TssBaseTransplantSchema { + default void restart(@NonNull final MigrationContext ctx) { + requireNonNull(ctx); + if (!ctx.appConfig().getConfigData(TssConfig.class).keyCandidateRoster()) { + return; + } + ctx.startupNetworks().overrideNetworkFor(ctx.roundNumber()).ifPresent(network -> { + setEncryptionKeys(network, ctx.newStates().get(TSS_ENCRYPTION_KEYS_KEY)); + setTssMessageOpsAndVotes( + network, + ctx.newStates().get(TSS_MESSAGE_MAP_KEY), + ctx.newStates().get(TSS_VOTE_MAP_KEY)); + }); + } + + /** + * Set the encryption keys in the state from the provided network, for whatever nodes they are available. + * @param network the network from which to extract the encryption keys + * @param encryptionKeys the state in which to store the encryption keys + */ + default void setEncryptionKeys( + @NonNull final Network network, + @NonNull final WritableKVState encryptionKeys) { + network.nodeMetadata().forEach(metadata -> { + if (metadata.tssEncryptionKey().length() > 0) { + final var key = new EntityNumber(metadata.rosterEntryOrThrow().nodeId()); + final var value = new TssEncryptionKeys(metadata.tssEncryptionKey(), Bytes.EMPTY); + encryptionKeys.put(key, value); + } + }); + } + + /** + * Set TSS state from the provided network. If {@link Network#tssMessages()} is empty, this is a no-op. If + * {@link Network#tssMessages()} is non-empty, then assumes there are exactly a threshold number of TSS messages + * that were received in ea + * + * @param network the network from which to extract the TSS messages + * @param tssMessageOps the state in which to store the TSS messages + */ + default void setTssMessageOpsAndVotes( + @NonNull final Network network, + @NonNull final WritableKVState tssMessageOps, + @NonNull final WritableKVState tssVotes) { + final var ops = network.tssMessages(); + if (ops.isEmpty()) { + return; + } + // We treat these messages as having come in this exact order + final AtomicLong seqNo = new AtomicLong(0); + final var roster = RosterUtils.rosterFrom(network); + final var rosterHash = RosterUtils.hash(roster).getBytes(); + network.tssMessages() + .forEach(op -> tssMessageOps.put(new TssMessageMapKey(rosterHash, seqNo.getAndIncrement()), op)); + final var tssVote = new BitSet(); + for (int i = 0, n = ops.size(); i < n; i++) { + tssVote.set(i); + } + network.nodeMetadata() + .forEach(metadata -> tssVotes.put( + new TssVoteMapKey( + rosterHash, metadata.rosterEntryOrThrow().nodeId()), + TssVoteTransactionBody.newBuilder() + .ledgerId(network.ledgerId()) + // (FUTURE) Are there any environments that would want a real signature here? + .nodeSignature(Bytes.EMPTY) + // (FUTURE) Are there any environments that would want a real source roster hash here? + .sourceRosterHash(Bytes.EMPTY) + .targetRosterHash(rosterHash) + .tssVote(Bytes.wrap(tssVote.toByteArray())) + .build())); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/schemas/V0570TssBaseSchema.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/schemas/V0570TssBaseSchema.java deleted file mode 100644 index 0b9bf4ce5423..000000000000 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/schemas/V0570TssBaseSchema.java +++ /dev/null @@ -1,76 +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.schemas; - -import static com.hedera.hapi.node.state.tss.RosterToKey.ACTIVE_ROSTER; -import static com.hedera.hapi.node.state.tss.TssKeyingStatus.WAITING_FOR_ENCRYPTION_KEYS; - -import com.hedera.hapi.node.base.SemanticVersion; -import com.hedera.hapi.node.state.common.EntityNumber; -import com.hedera.hapi.node.state.tss.TssStatus; -import com.hedera.hapi.services.auxiliary.tss.TssEncryptionKeyTransactionBody; -import com.hedera.pbj.runtime.io.buffer.Bytes; -import com.swirlds.state.lifecycle.MigrationContext; -import com.swirlds.state.lifecycle.Schema; -import com.swirlds.state.lifecycle.StateDefinition; -import edu.umd.cs.findbugs.annotations.NonNull; -import java.util.Set; - -/** - * Schema for the TSS service. - */ -public class V0570TssBaseSchema extends Schema { - public static final String TSS_STATUS_KEY = "TSS_STATUS"; - public static final String TSS_ENCRYPTION_KEY_MAP_KEY = "TSS_ENCRYPTION_KEY"; - /** - * This will at most be equal to the number of nodes in the network. - */ - private static final long MAX_TSS_ENCRYPTION_KEYS = 65_536L; - - /** - * The version of the schema. - */ - private static final SemanticVersion VERSION = - SemanticVersion.newBuilder().major(0).minor(57).patch(0).build(); - - /** - * Create a new instance - */ - public V0570TssBaseSchema() { - super(VERSION); - } - - @Override - public void migrate(@NonNull final MigrationContext ctx) { - final var tssStatusState = ctx.newStates().getSingleton(TSS_STATUS_KEY); - if (tssStatusState.get() == null) { - tssStatusState.put(new TssStatus(WAITING_FOR_ENCRYPTION_KEYS, ACTIVE_ROSTER, Bytes.EMPTY)); - } - } - - @NonNull - @Override - public Set statesToCreate() { - return Set.of( - StateDefinition.singleton(TSS_STATUS_KEY, TssStatus.PROTOBUF), - StateDefinition.onDisk( - TSS_ENCRYPTION_KEY_MAP_KEY, - EntityNumber.PROTOBUF, - TssEncryptionKeyTransactionBody.PROTOBUF, - MAX_TSS_ENCRYPTION_KEYS)); - } -} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/schemas/V0580TssBaseSchema.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/schemas/V0580TssBaseSchema.java new file mode 100644 index 000000000000..322d5883ada4 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/schemas/V0580TssBaseSchema.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.schemas; + +import com.hedera.hapi.node.base.SemanticVersion; +import com.hedera.hapi.node.state.common.EntityNumber; +import com.hedera.hapi.node.state.tss.TssEncryptionKeys; +import com.swirlds.state.lifecycle.MigrationContext; +import com.swirlds.state.lifecycle.Schema; +import com.swirlds.state.lifecycle.StateDefinition; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Set; + +/** + * Schema for the TSS service. + */ +public class V0580TssBaseSchema extends Schema implements TssBaseTransplantSchema { + public static final String TSS_ENCRYPTION_KEYS_KEY = "TSS_ENCRYPTION_KEYS"; + /** + * This will at most be equal to the number of nodes in the network. + */ + private static final long MAX_TSS_ENCRYPTION_KEYS = 65_536L; + + /** + * The version of the schema. + */ + private static final SemanticVersion VERSION = + SemanticVersion.newBuilder().major(0).minor(58).patch(0).build(); + + public V0580TssBaseSchema() { + super(VERSION); + } + + @Override + public @NonNull Set statesToCreate() { + return Set.of(StateDefinition.onDisk( + TSS_ENCRYPTION_KEYS_KEY, EntityNumber.PROTOBUF, TssEncryptionKeys.PROTOBUF, MAX_TSS_ENCRYPTION_KEYS)); + } + + @Override + public void restart(@NonNull final MigrationContext ctx) { + TssBaseTransplantSchema.super.restart(ctx); + } +} 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 index e65b3b5ecde2..933aace5fc8d 100644 --- 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 @@ -20,10 +20,9 @@ import static java.util.stream.Collectors.toMap; import com.hedera.hapi.node.state.roster.RosterEntry; +import com.hedera.hapi.node.state.tss.TssEncryptionKeys; import com.hedera.hapi.node.state.tss.TssMessageMapKey; -import com.hedera.hapi.node.state.tss.TssStatus; import com.hedera.hapi.node.state.tss.TssVoteMapKey; -import com.hedera.hapi.services.auxiliary.tss.TssEncryptionKeyTransactionBody; import com.hedera.hapi.services.auxiliary.tss.TssMessageTransactionBody; import com.hedera.hapi.services.auxiliary.tss.TssVoteTransactionBody; import com.hedera.pbj.runtime.io.buffer.Bytes; @@ -31,6 +30,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.util.BitSet; +import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.function.LongUnaryOperator; @@ -39,8 +39,9 @@ public interface ReadableTssStore { /** * The selected TSS messages and implied ledger id for some roster. + * * @param tssMessages the selected TSS messages - * @param ledgerId the implied ledger id + * @param ledgerId the implied ledger id */ record RosterKeys(@NonNull List tssMessages, @NonNull Bytes ledgerId) { public RosterKeys { @@ -54,7 +55,7 @@ record RosterKeys(@NonNull List tssMessages, @NonNull * * @param sourceRosterHash the source roster hash * @param targetRosterHash the target roster hash - * @param rosterStore the roster store + * @param rosterStore the roster store */ default Optional consensusRosterKeys( @NonNull final Bytes sourceRosterHash, @@ -80,7 +81,7 @@ default Optional consensusRosterKeys( * * @param sourceRosterHash the source roster hash * @param targetRosterHash the target roster hash - * @param rosterStore the roster store + * @param rosterStore the roster store * @return the roster keys, if available */ default Optional anyWinningVoteFrom( @@ -108,14 +109,44 @@ default Optional anyWinningVoteFrom( return anyWinningVoteFrom(sourceRosterHash, targetRosterHash, sourceRosterWeight, nodeWeightFn); } + /** + * If present, returns one of the winning votes from the given target roster hash. + * This iterates through all the votes for the target roster hash and returns the first one that is valid for + * any source roster. + * + * @param targetRosterHash the target roster hash + * @param rosterStore the roster store + * @return the winning vote, if present + */ + default Optional anyWinningVoteFor( + @NonNull final Bytes targetRosterHash, @NonNull final ReadableRosterStore rosterStore) { + requireNonNull(targetRosterHash); + requireNonNull(rosterStore); + final var possibleSourceRosters = new HashSet(); + final var votesForTargetRoster = allVotes().stream() + .filter(vote -> targetRosterHash.equals(vote.targetRosterHash())) + .toList(); + for (final var vote : votesForTargetRoster) { + possibleSourceRosters.add(vote.sourceRosterHash()); + } + + for (final var sourceRosterHash : possibleSourceRosters) { + final var vote = anyWinningVoteFrom(sourceRosterHash, targetRosterHash, rosterStore); + if (vote.isPresent()) { + return vote; + } + } + return Optional.empty(); + } + /** * If present, returns one of the winning votes from the given source roster hash for the keys of the target roster, * using the given total weight and per-node weight for the source roster. There is no guarantee of ordering between * multiple winning votes. * - * @param sourceRosterHash the source roster hash the vote must be from - * @param targetRosterHash the target roster hash the vote must be for - * @param sourceRosterWeight the total weight of the source the vote must be from + * @param sourceRosterHash the source roster hash the vote must be from + * @param targetRosterHash the target roster hash the vote must be for + * @param sourceRosterWeight the total weight of the source the vote must be from * @param sourceRosterWeightFn a function that returns the weight of a node in the source roster given its id * @return a winning vote, if present */ @@ -149,6 +180,13 @@ Optional anyWinningVoteFrom( */ TssVoteTransactionBody getVote(@NonNull TssVoteMapKey tssVoteMapKey); + /** + * Get all TSS votes in the store. This is needed for a specific case when we are checking the status + * for the keying of an active roster. + * @return The list of TSS votes. + */ + List allVotes(); + /** * Check if a TSS vote exists for the given key. * @@ -159,6 +197,7 @@ Optional anyWinningVoteFrom( /** * Get the list of Tss messages for the given roster hash. + * * @param rosterHash The roster hash to look up. * @return The list of Tss messages, or an empty list if not found. */ @@ -166,16 +205,10 @@ Optional anyWinningVoteFrom( /** * Get the Tss encryption key transaction body for the given node ID. + * * @param nodeID The node ID to look up. * @return The Tss encryption key transaction body, or null if not found. */ @Nullable - TssEncryptionKeyTransactionBody getTssEncryptionKey(final long nodeID); - - /** - * Get the Tss status. - * @return The Tss status. - */ - @NonNull - TssStatus getTssStatus(); + TssEncryptionKeys getTssEncryptionKeys(long nodeID); } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/stores/ReadableTssStoreImpl.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/stores/ReadableTssStoreImpl.java index 8dec56ceb64d..0fb99c9ab08c 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/stores/ReadableTssStoreImpl.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/stores/ReadableTssStoreImpl.java @@ -19,25 +19,20 @@ import static com.hedera.node.app.tss.handlers.TssUtils.hasMetThreshold; 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 com.hedera.node.app.tss.schemas.V0570TssBaseSchema.TSS_ENCRYPTION_KEY_MAP_KEY; -import static com.hedera.node.app.tss.schemas.V0570TssBaseSchema.TSS_STATUS_KEY; +import static com.hedera.node.app.tss.schemas.V0580TssBaseSchema.TSS_ENCRYPTION_KEYS_KEY; import static java.util.Objects.requireNonNull; -import static java.util.Spliterator.NONNULL; -import static java.util.Spliterators.spliterator; import static java.util.stream.Collectors.groupingBy; import static java.util.stream.Collectors.toList; -import static java.util.stream.StreamSupport.stream; +import com.google.common.collect.Streams; import com.hedera.hapi.node.state.common.EntityNumber; +import com.hedera.hapi.node.state.tss.TssEncryptionKeys; import com.hedera.hapi.node.state.tss.TssMessageMapKey; -import com.hedera.hapi.node.state.tss.TssStatus; import com.hedera.hapi.node.state.tss.TssVoteMapKey; -import com.hedera.hapi.services.auxiliary.tss.TssEncryptionKeyTransactionBody; import com.hedera.hapi.services.auxiliary.tss.TssMessageTransactionBody; import com.hedera.hapi.services.auxiliary.tss.TssVoteTransactionBody; import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.state.spi.ReadableKVState; -import com.swirlds.state.spi.ReadableSingletonState; import com.swirlds.state.spi.ReadableStates; import edu.umd.cs.findbugs.annotations.NonNull; import java.util.ArrayList; @@ -56,8 +51,7 @@ public class ReadableTssStoreImpl implements ReadableTssStore { private final ReadableKVState readableTssVoteState; - private final ReadableKVState readableTssEncryptionKeyState; - private final ReadableSingletonState readableTssStatusState; + private final ReadableKVState readableTssEncryptionKeyState; /** * Create a new {@link ReadableTssStoreImpl} instance. @@ -68,8 +62,7 @@ public ReadableTssStoreImpl(@NonNull final ReadableStates states) { requireNonNull(states); this.readableTssMessageState = states.get(TSS_MESSAGE_MAP_KEY); this.readableTssVoteState = states.get(TSS_VOTE_MAP_KEY); - this.readableTssEncryptionKeyState = states.get(TSS_ENCRYPTION_KEY_MAP_KEY); - this.readableTssStatusState = states.getSingleton(TSS_STATUS_KEY); + this.readableTssEncryptionKeyState = states.get(TSS_ENCRYPTION_KEYS_KEY); } @Override @@ -81,7 +74,7 @@ public Optional anyWinningVoteFrom( requireNonNull(sourceRosterHash); requireNonNull(targetRosterHash); requireNonNull(sourceRosterWeightFn); - return stream(spliterator(readableTssVoteState.keys(), readableTssVoteState.size(), NONNULL), false) + return Streams.stream(readableTssVoteState.keys()) .filter(key -> targetRosterHash.equals(key.rosterHash())) .map(key -> new WeightedVote( sourceRosterWeightFn.applyAsLong(key.nodeId()), requireNonNull(readableTssVoteState.get(key)))) @@ -129,6 +122,16 @@ public TssVoteTransactionBody getVote(@NonNull final TssVoteMapKey tssVoteKey) { return readableTssVoteState.get(tssVoteKey); } + /** + * {@inheritDoc} + */ + @Override + public List allVotes() { + return Streams.stream(readableTssVoteState.keys()) + .map(readableTssVoteState::get) + .toList(); + } + /** * {@inheritDoc} */ @@ -153,14 +156,8 @@ public List getMessagesForTarget(@NonNull final Bytes * {@inheritDoc} */ @Override - public TssEncryptionKeyTransactionBody getTssEncryptionKey(long nodeID) { + public TssEncryptionKeys getTssEncryptionKeys(final long nodeID) { return readableTssEncryptionKeyState.get( EntityNumber.newBuilder().number(nodeID).build()); } - - @Override - @NonNull - public TssStatus getTssStatus() { - return readableTssStatusState.get(); - } } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/stores/WritableTssStore.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/stores/WritableTssStore.java index 4a74374b262c..5008bc30516d 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/stores/WritableTssStore.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/stores/WritableTssStore.java @@ -18,19 +18,16 @@ 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 com.hedera.node.app.tss.schemas.V0570TssBaseSchema.TSS_ENCRYPTION_KEY_MAP_KEY; -import static com.hedera.node.app.tss.schemas.V0570TssBaseSchema.TSS_STATUS_KEY; +import static com.hedera.node.app.tss.schemas.V0580TssBaseSchema.TSS_ENCRYPTION_KEYS_KEY; import static java.util.Objects.requireNonNull; import com.hedera.hapi.node.state.common.EntityNumber; import com.hedera.hapi.node.state.tss.TssMessageMapKey; -import com.hedera.hapi.node.state.tss.TssStatus; import com.hedera.hapi.node.state.tss.TssVoteMapKey; import com.hedera.hapi.services.auxiliary.tss.TssEncryptionKeyTransactionBody; 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.WritableSingletonState; import com.swirlds.state.spi.WritableStates; import edu.umd.cs.findbugs.annotations.NonNull; @@ -47,14 +44,11 @@ public class WritableTssStore extends ReadableTssStoreImpl { private final WritableKVState tssEncryptionKeyState; - private final WritableSingletonState tssStatusState; - public WritableTssStore(@NonNull final WritableStates states) { super(states); this.tssMessageState = states.get(TSS_MESSAGE_MAP_KEY); this.tssVoteState = states.get(TSS_VOTE_MAP_KEY); - this.tssEncryptionKeyState = states.get(TSS_ENCRYPTION_KEY_MAP_KEY); - this.tssStatusState = states.getSingleton(TSS_STATUS_KEY); + this.tssEncryptionKeyState = states.get(TSS_ENCRYPTION_KEYS_KEY); } public void put(@NonNull final TssMessageMapKey tssMessageMapKey, @NonNull final TssMessageTransactionBody txBody) { @@ -75,11 +69,6 @@ public void put(@NonNull final EntityNumber entityNumber, @NonNull final TssEncr tssEncryptionKeyState.put(entityNumber, txBody); } - public void put(@NonNull final TssStatus tssStatus) { - requireNonNull(tssStatus); - tssStatusState.put(tssStatus); - } - public void remove(@NonNull final TssMessageMapKey tssMessageMapKey) { requireNonNull(tssMessageMapKey); tssMessageState.remove(tssMessageMapKey); @@ -99,6 +88,5 @@ public void clear() { tssVoteState.keys().forEachRemaining(tssVoteState::remove); tssMessageState.keys().forEachRemaining(tssMessageState::remove); tssEncryptionKeyState.keys().forEachRemaining(tssEncryptionKeyState::remove); - tssStatusState.put(TssStatus.DEFAULT); } } 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 38a71a29ca3c..dadd26d316ab 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 @@ -225,7 +225,7 @@ public void handleRound(@NonNull final State state, @NonNull final Round round) logStartRound(round); cacheWarmer.warm(state, round); if (configProvider.getConfiguration().getConfigData(TssConfig.class).keyCandidateRoster()) { - tssBaseService.generateParticipantDirectory(state); + tssBaseService.ensureParticipantDirectoryKnown(state); } if (streamMode != RECORDS) { blockStreamManager.startRound(round, state); @@ -253,6 +253,7 @@ public void handleRound(@NonNull final State state, @NonNull final Round round) /** * Applies all effects of the events in the given round to the given state, writing stream items * that capture these effects in the process. + * * @param state the state to apply the effects to * @param round the round to apply the effects of */ @@ -319,9 +320,9 @@ private void handleEvents(@NonNull final State state, @NonNull final Round round * executing the workflow for the transaction. This produces a stream of records that are then passed to the * {@link BlockRecordManager} to be externalized. * - * @param state the writable {@link State} that this transaction will work on - * @param creator the {@link NodeInfo} of the creator of the transaction - * @param txn the {@link ConsensusTransaction} to be handled + * @param state the writable {@link State} that this transaction will work on + * @param creator the {@link NodeInfo} of the creator of the transaction + * @param txn the {@link ConsensusTransaction} to be handled * @param txnVersion the software version for the event containing the transaction */ private void handlePlatformTransaction( @@ -396,11 +397,12 @@ private void handlePlatformTransaction( * As a side effect on the workflow internal state, updates the {@link BlockStreamManager}'s last interval process * time to the latest time known to have been processed; and the {@link #lastExecutedSecond} value to the last * second of the interval for which all scheduled transactions were executed. - * @param state the state to execute scheduled transactions from + * + * @param state the state to execute scheduled transactions from * @param executionStart the start of the interval to execute transactions in - * @param consensusNow the consensus time at which the user transaction triggering this execution was processed - * @param creatorInfo the node info of the user transaction creator - * @param type the type of the user transaction triggering this execution + * @param consensusNow the consensus time at which the user transaction triggering this execution was processed + * @param creatorInfo the node info of the user transaction creator + * @param type the type of the user transaction triggering this execution */ private void executeAsManyScheduled( @NonNull final State state, @@ -481,9 +483,9 @@ private void executeAsManyScheduled( * Type inference helper to compute the base builder for a {@link UserTxn} derived from a * {@link ExecutableTxn}. * - * @param the type of the stream builder + * @param the type of the stream builder * @param executableTxn the executable transaction to compute the base builder for - * @param userTxn the user transaction derived from the executable transaction + * @param userTxn the user transaction derived from the executable transaction * @return the base builder for the user transaction */ private T baseBuilderFor( @@ -496,9 +498,10 @@ private T baseBuilderFor( * Purges all service state used for scheduling work that was expired by the last time the purge * was triggered; but is not expired at the current time. Returns true if the last purge time * should be set to the current time. + * * @param state the state to purge - * @param then the last time the purge was triggered - * @param now the current time + * @param then the last time the purge was triggered + * @param now the current time */ private void purgeScheduling(@NonNull final State state, final Instant then, final Instant now) { if (!Instant.EPOCH.equals(then) && then.getEpochSecond() < now.getEpochSecond()) { @@ -519,7 +522,8 @@ private void purgeScheduling(@NonNull final State state, final Instant then, fin * there is an internal error when executing the transaction, returns stream output of * just the transaction with a {@link ResponseCodeEnum#FAIL_INVALID} transaction result, * and no other side effects. - * @param userTxn the user transaction to execute + * + * @param userTxn the user transaction to execute * @param txnVersion the software version for the event containing the transaction * @return the stream output from executing the transaction */ @@ -615,7 +619,8 @@ private HandleOutput executeTopLevel(@NonNull final UserTxn userTxn, @NonNull fi * there is an internal error when executing the transaction, returns stream output of just the * scheduled transaction with a {@link ResponseCodeEnum#FAIL_INVALID} transaction result, and * no other side effects. - * @param state the state to execute the transaction against + * + * @param state the state to execute the transaction against * @param consensusNow the time to execute the transaction at * @return the stream output from executing the transaction */ @@ -649,7 +654,8 @@ private HandleOutput executeScheduled( /** * Manages time-based side effects for the given user transaction and dispatch. - * @param userTxn the user transaction to manage time for + * + * @param userTxn the user transaction to manage time for * @param dispatch the dispatch to manage time for */ private void advanceTimeFor(@NonNull final UserTxn userTxn, @NonNull final Dispatch dispatch) { @@ -657,8 +663,10 @@ private void advanceTimeFor(@NonNull final UserTxn userTxn, @NonNull final Dispa // correctly detect stake period boundary, so the order of the following two lines is important processStakePeriodChanges(userTxn, dispatch); if (isNextSecond(userTxn.consensusNow(), blockStreamManager.lastHandleTime())) { - // Check if the tss encryption keys are present in the state and reached threshold - tssBaseService.manageTssStatus(userTxn.stack()); + // Check the tss status and manage it if necessary + final var isStakePeriodBoundary = processStakePeriodChanges(userTxn, dispatch); + tssBaseService.manageTssStatus( + userTxn.stack(), isStakePeriodBoundary, userTxn.consensusNow(), storeMetricsService); } blockStreamManager.setLastHandleTime(userTxn.consensusNow()); if (streamMode != BLOCKS) { @@ -670,9 +678,10 @@ private void advanceTimeFor(@NonNull final UserTxn userTxn, @NonNull final Dispa /** * Commits an action with side effects while capturing its key/value state changes and writing them to the * block stream. + * * @param writableStates the writable states to commit the action to - * @param now the consensus timestamp of the action - * @param action the action to commit + * @param now the consensus timestamp of the action + * @param action the action to commit */ private void doStreamingKVChanges( @NonNull final WritableStates writableStates, @NonNull final Instant now, @NonNull final Runnable action) { @@ -794,12 +803,13 @@ public static StreamBuilder initializeBuilderInfo( /** * Processes any side effects of crossing a stake period boundary. - * @param userTxn the user transaction that crossed the boundary + * + * @param userTxn the user transaction that crossed the boundary * @param dispatch the dispatch for the user transaction that crossed the boundary */ - private void processStakePeriodChanges(@NonNull final UserTxn userTxn, @NonNull final Dispatch dispatch) { + private boolean processStakePeriodChanges(@NonNull final UserTxn userTxn, @NonNull final Dispatch dispatch) { try { - stakePeriodChanges.process( + return stakePeriodChanges.process( dispatch, userTxn.stack(), userTxn.tokenContextImpl(), @@ -812,6 +822,7 @@ private void processStakePeriodChanges(@NonNull final UserTxn userTxn, @NonNull // get back to user transactions logger.error("Failed to process stake period changes", e); } + return false; } private static void logPreDispatch(@NonNull final UserTxn userTxn) { diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/steps/StakePeriodChanges.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/steps/StakePeriodChanges.java index 43249ff67cf6..150e932418ad 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/steps/StakePeriodChanges.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/steps/StakePeriodChanges.java @@ -99,7 +99,7 @@ public StakePeriodChanges( * @param isGenesis whether the current transaction is the genesis transaction * @param lastHandleTime the last instant at which a transaction was handled */ - public void process( + public boolean process( @NonNull final Dispatch dispatch, @NonNull final SavepointStackImpl stack, @NonNull final TokenContext tokenContext, @@ -111,7 +111,8 @@ public void process( requireNonNull(tokenContext); requireNonNull(streamMode); requireNonNull(lastHandleTime); - if (isGenesis || isStakingPeriodBoundary(streamMode, tokenContext, lastHandleTime)) { + final var isStakePeriodBoundary = isStakingPeriodBoundary(streamMode, tokenContext, lastHandleTime); + if (isGenesis || isStakePeriodBoundary) { try { exchangeRateManager.updateMidnightRates(stack); stack.commitSystemStateChanges(); @@ -141,6 +142,7 @@ public void process( startKeyingCandidateRoster(dispatch.handleContext(), newWritableRosterStore(stack, config)); } } + return !isGenesis && isStakePeriodBoundary; } private boolean isStakingPeriodBoundary( diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/standalone/impl/StandaloneNetworkInfo.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/standalone/impl/StandaloneNetworkInfo.java index 8885d13e591f..02b547bba8b6 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/standalone/impl/StandaloneNetworkInfo.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/standalone/impl/StandaloneNetworkInfo.java @@ -23,7 +23,6 @@ import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.NodeAddressBook; -import com.hedera.hapi.node.state.roster.Roster; import com.hedera.node.app.info.NodeInfoImpl; import com.hedera.node.config.ConfigProvider; import com.hedera.node.config.data.FilesConfig; @@ -133,11 +132,6 @@ public void updateFrom(final State state) { throw new UnsupportedOperationException("Not implemented"); } - @Override - public Roster roster() { - throw new UnsupportedOperationException("Not implemented"); - } - private @NonNull List nodeInfosOrThrow() { return requireNonNull(nodeInfos, "Not initialized"); } diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/StartupAssetsTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/StartupAssetsTest.java deleted file mode 100644 index 95835bc1de51..000000000000 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/StartupAssetsTest.java +++ /dev/null @@ -1,117 +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; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.hedera.node.internal.network.Network; -import java.nio.file.Path; -import java.util.Optional; -import org.junit.jupiter.api.Test; - -public class StartupAssetsTest { - - private static class MockFactory implements StartupAssets.Factory { - @Override - public StartupAssets fromInitialConditions(Path workingDir) { - return mock(StartupAssets.class); - } - } - - @Test - void testFactoryFromInitialConditions() { - Path workingDir = mock(Path.class); - MockFactory factory = new MockFactory(); - StartupAssets result = factory.fromInitialConditions(workingDir); - assertThat(result).isNotNull(); - } - - @Test - void testMigrationNetworkOrThrowReturnsNetwork() { - StartupAssets startupAssets = mock(StartupAssets.class); - Network mockNetwork = mock(Network.class); - when(startupAssets.migrationNetworkOrThrow()).thenReturn(mockNetwork); - Network result = startupAssets.migrationNetworkOrThrow(); - - assertThat(result).isNotNull(); - assertThat(mockNetwork).isEqualTo(result); - } - - @Test - void testMigrationNetworkOrThrowThrowsException() { - StartupAssets startupAssets = mock(StartupAssets.class); - when(startupAssets.migrationNetworkOrThrow()) - .thenThrow(new UnsupportedOperationException("Migration not supported")); - assertThatThrownBy(startupAssets::migrationNetworkOrThrow) - .isInstanceOf(UnsupportedOperationException.class) - .hasMessage("Migration not supported"); - } - - @Test - void testGenesisNetworkOrThrowReturnsNetwork() { - StartupAssets startupAssets = mock(StartupAssets.class); - Network mockNetwork = mock(Network.class); - - when(startupAssets.genesisNetworkOrThrow()).thenReturn(mockNetwork); - Network result = startupAssets.genesisNetworkOrThrow(); - assertThat(result).isNotNull().isEqualTo(mockNetwork); - } - - @Test - void testGenesisNetworkOrThrowThrowsException() { - StartupAssets startupAssets = mock(StartupAssets.class); - when(startupAssets.genesisNetworkOrThrow()) - .thenThrow(new UnsupportedOperationException("Genesis network not supported")); - assertThatThrownBy(startupAssets::genesisNetworkOrThrow) - .isInstanceOf(UnsupportedOperationException.class) - .hasMessage("Genesis network not supported"); - } - - @Test - void testOverrideNetworkReturnsNetwork() { - StartupAssets startupAssets = mock(StartupAssets.class); - Network mockNetwork = mock(Network.class); - long roundNumber = 123L; - when(startupAssets.overrideNetwork(roundNumber)).thenReturn(Optional.of(mockNetwork)); - Optional result = startupAssets.overrideNetwork(roundNumber); - assertThat(result).isPresent().contains(mockNetwork); - } - - @Test - void testOverrideNetworkReturnsEmptyOptional() { - StartupAssets startupAssets = mock(StartupAssets.class); - long roundNumber = 123L; - when(startupAssets.overrideNetwork(roundNumber)).thenReturn(Optional.empty()); - Optional result = startupAssets.overrideNetwork(roundNumber); - assertThat(result).isNotPresent(); - } - - @Test - void testArchiveInitialConditionsExecutesSuccessfully() { - StartupAssets startupAssets = mock(StartupAssets.class); - doNothing().when(startupAssets).archiveInitialConditions(); - assertThatCode(() -> startupAssets.archiveInitialConditions()).doesNotThrowAnyException(); - verify(startupAssets, times(1)).archiveInitialConditions(); - } -} diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/schemas/V0560BlockStreamSchemaTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/schemas/V0560BlockStreamSchemaTest.java index ebbbc5d65be4..d9cc1b910ce2 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/schemas/V0560BlockStreamSchemaTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/schemas/V0560BlockStreamSchemaTest.java @@ -82,6 +82,7 @@ void createsDefaultInfoAtGenesis() { given(migrationContext.newStates()).willReturn(writableStates); given(writableStates.getSingleton(BLOCK_STREAM_INFO_KEY)) .willReturn(state); + given(migrationContext.isGenesis()).willReturn(true); subject.restart(migrationContext); @@ -135,7 +136,6 @@ void assumesMigrationIfNotGenesisAndStateIsNull() { @Test void migrationIsNoopIfNotGenesisAndInfoIsNonNull() { given(migrationContext.newStates()).willReturn(writableStates); - given(migrationContext.previousVersion()).willReturn(SemanticVersion.DEFAULT); given(writableStates.getSingleton(BLOCK_STREAM_INFO_KEY)) .willReturn(state); given(state.get()).willReturn(BlockStreamInfo.DEFAULT); diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/info/DiskStartupNetworksTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/info/DiskStartupNetworksTest.java index af99b0b8bd14..1a5efe9c8157 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/info/DiskStartupNetworksTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/info/DiskStartupNetworksTest.java @@ -20,15 +20,17 @@ import static com.hedera.node.app.info.DiskStartupNetworks.ARCHIVE; import static com.hedera.node.app.info.DiskStartupNetworks.GENESIS_NETWORK_JSON; import static com.hedera.node.app.info.DiskStartupNetworks.OVERRIDE_NETWORK_JSON; +import static com.hedera.node.app.info.DiskStartupNetworks.fromLegacyAddressBook; +import static com.hedera.node.app.roster.schemas.V0540RosterSchema.ROSTER_KEY; +import static com.hedera.node.app.roster.schemas.V0540RosterSchema.ROSTER_STATES_KEY; import static com.hedera.node.app.service.addressbook.impl.schemas.V053AddressBookSchema.NODES_KEY; import static com.hedera.node.app.spi.AppContext.Gossip.UNAVAILABLE_GOSSIP; 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 com.hedera.node.app.tss.schemas.V0570TssBaseSchema.TSS_ENCRYPTION_KEY_MAP_KEY; +import static com.hedera.node.app.tss.schemas.V0580TssBaseSchema.TSS_ENCRYPTION_KEYS_KEY; import static com.hedera.node.app.workflows.standalone.TransactionExecutorsTest.FAKE_NETWORK_INFO; import static com.hedera.node.app.workflows.standalone.TransactionExecutorsTest.NO_OP_METRICS; -import static com.swirlds.platform.state.service.schemas.V0540RosterSchema.ROSTER_KEY; -import static com.swirlds.platform.state.service.schemas.V0540RosterSchema.ROSTER_STATES_KEY; +import static com.swirlds.platform.state.service.PlatformStateService.PLATFORM_STATE_SERVICE; import static java.util.Collections.emptyList; import static java.util.Objects.requireNonNull; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; @@ -43,10 +45,9 @@ import com.hedera.hapi.node.state.roster.Roster; import com.hedera.hapi.node.state.roster.RosterState; import com.hedera.hapi.node.state.roster.RoundRosterPair; +import com.hedera.hapi.node.state.tss.TssEncryptionKeys; import com.hedera.hapi.node.state.tss.TssMessageMapKey; -import com.hedera.hapi.node.state.tss.TssStatus; import com.hedera.hapi.node.state.tss.TssVoteMapKey; -import com.hedera.hapi.services.auxiliary.tss.TssEncryptionKeyTransactionBody; import com.hedera.hapi.services.auxiliary.tss.TssMessageTransactionBody; import com.hedera.hapi.services.auxiliary.tss.TssVoteTransactionBody; import com.hedera.node.app.config.BootstrapConfigProviderImpl; @@ -62,7 +63,6 @@ import com.hedera.node.app.tss.TssBaseService; import com.hedera.node.app.tss.TssBaseServiceImpl; import com.hedera.node.app.tss.api.TssLibrary; -import com.hedera.node.app.tss.schemas.V0570TssBaseSchema; import com.hedera.node.app.version.ServicesSoftwareVersion; import com.hedera.node.config.ConfigProvider; import com.hedera.node.config.VersionedConfigImpl; @@ -74,8 +74,13 @@ import com.hedera.pbj.runtime.io.buffer.Bytes; import com.hedera.pbj.runtime.io.stream.ReadableStreamingData; import com.hedera.pbj.runtime.io.stream.WritableStreamingData; +import com.swirlds.common.platform.NodeId; import com.swirlds.platform.roster.RosterUtils; +import com.swirlds.platform.state.service.PlatformStateService; +import com.swirlds.platform.state.service.ReadablePlatformStateStore; import com.swirlds.platform.state.service.ReadableRosterStoreImpl; +import com.swirlds.platform.system.address.Address; +import com.swirlds.platform.system.address.AddressBook; import com.swirlds.state.State; import com.swirlds.state.lifecycle.StartupNetworks; import com.swirlds.state.spi.CommittableWritableStates; @@ -91,6 +96,7 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.ForkJoinPool; +import java.util.stream.IntStream; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -103,11 +109,11 @@ @ExtendWith(MockitoExtension.class) class DiskStartupNetworksTest { private static final int FAKE_NETWORK_SIZE = 4; - private static final long NODE_ID = 0L; private static final long ROUND_NO = 666L; private static final Bytes EXPECTED_LEDGER_ID = Bytes.fromBase64("Lw=="); private static final Comparator TSS_MESSAGE_COMPARATOR = Comparator.comparingLong(TssMessageTransactionBody::shareIndex); + private static final Bytes FAKE_ENCRYPTION_KEY = Bytes.fromBase64("ASM="); private static Network networkWithTssKeys; private static Network networkWithoutTssKeys; @@ -152,7 +158,7 @@ static void setupAll() throws IOException, ParseException { @BeforeEach void setUp() { - subject = new DiskStartupNetworks(NODE_ID, configProvider, tssBaseService); + subject = new DiskStartupNetworks(configProvider, tssBaseService); } @Test @@ -184,6 +190,33 @@ void findsAvailableMigrationNetwork() throws IOException { assertThat(network).isEqualTo(networkWithTssKeys); } + @Test + void computesFromLegacyAddressBook() { + final int n = 3; + final var legacyBook = new AddressBook(IntStream.range(0, n) + .mapToObj(i -> new Address( + NodeId.of(i), + "" + i, + "node" + (i + 1), + 1L, + "localhost", + i + 1, + "127.0.0.1", + i + 2, + null, + null, + "0.0." + (i + 3))) + .toList()); + final var network = fromLegacyAddressBook(legacyBook); + for (int i = 0; i < n; i++) { + final var rosterEntry = network.nodeMetadata().get(i).rosterEntryOrThrow(); + assertThat(rosterEntry.nodeId()).isEqualTo(i); + assertThat(rosterEntry.gossipEndpoint().getFirst().ipAddressV4()) + .isEqualTo(Bytes.wrap(new byte[] {127, 0, 0, 1})); + assertThat(rosterEntry.gossipEndpoint().getLast().domainName()).isEqualTo("localhost"); + } + } + @Test void archivesGenesisNetworks() throws IOException { givenConfig(); @@ -304,7 +337,17 @@ private State stateContainingInfoFrom(@NonNull final Network network) { tssLibrary, ForkJoinPool.commonPool(), NO_OP_METRICS); - Set.of(tssBaseService, new EntityIdService(), new RosterService(roster -> true), new AddressBookServiceImpl()) + PLATFORM_STATE_SERVICE.setAppVersionFn(ServicesSoftwareVersion::from); + PLATFORM_STATE_SERVICE.setDiskAddressBook(new AddressBook()); + Set.of( + tssBaseService, + PLATFORM_STATE_SERVICE, + new EntityIdService(), + new RosterService( + roster -> true, + () -> new ReadablePlatformStateStore( + state.getReadableStates(PlatformStateService.NAME))), + new AddressBookServiceImpl()) .forEach(servicesRegistry::register); final var migrator = new FakeServiceMigrator(); final var bootstrapConfig = new BootstrapConfigProviderImpl().getConfiguration(); @@ -374,18 +417,13 @@ private void addTssInfo(@NonNull final FakeState state, @NonNull final Network n tssVotes.put(key, vote); } - final var tssEncryptionKey = - writableStates.get(TSS_ENCRYPTION_KEY_MAP_KEY); + final var tssEncryptionKey = writableStates.get(TSS_ENCRYPTION_KEYS_KEY); for (int i = 0; i < FAKE_NETWORK_SIZE; i++) { final var key = new EntityNumber(i); - final var value = TssEncryptionKeyTransactionBody.newBuilder() - .publicTssEncryptionKey(Bytes.EMPTY) - .build(); + final var value = new TssEncryptionKeys(FAKE_ENCRYPTION_KEY, Bytes.EMPTY); tssEncryptionKey.put(key, value); } - final var tssStatus = writableStates.getSingleton(V0570TssBaseSchema.TSS_STATUS_KEY); - tssStatus.put(TssStatus.DEFAULT); ((CommittableWritableStates) writableStates).commit(); } diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/info/StateNetworkInfoTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/info/StateNetworkInfoTest.java index 799a67679274..da13924b53b8 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/info/StateNetworkInfoTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/info/StateNetworkInfoTest.java @@ -16,7 +16,6 @@ package com.hedera.node.app.info; -import static com.hedera.node.app.workflows.standalone.TransactionExecutorsTest.getCertBytes; import static com.hedera.node.app.workflows.standalone.TransactionExecutorsTest.randomX509Certificate; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -32,9 +31,6 @@ import com.hedera.hapi.node.state.common.EntityNumber; import com.hedera.hapi.node.state.roster.Roster; import com.hedera.hapi.node.state.roster.RosterEntry; -import com.hedera.hapi.platform.state.Address; -import com.hedera.hapi.platform.state.AddressBook; -import com.hedera.hapi.platform.state.NodeId; import com.hedera.hapi.platform.state.PlatformState; import com.hedera.node.app.service.addressbook.AddressBookService; import com.hedera.node.config.ConfigProvider; @@ -90,7 +86,7 @@ public void setUp() { when(state.getReadableStates(AddressBookService.NAME)).thenReturn(readableStates); when(readableStates.get("NODES")).thenReturn(nodeState); when(state.getReadableStates(PlatformStateService.NAME)).thenReturn(readableStates); - networkInfo = new StateNetworkInfo(state, activeRoster, SELF_ID, configProvider); + networkInfo = new StateNetworkInfo(SELF_ID, state, activeRoster, configProvider); } @Test @@ -129,34 +125,6 @@ public void testContainsNode() { @Test public void testUpdateFrom() { when(nodeState.get(any(EntityNumber.class))).thenReturn(mock(Node.class)); - when(readableStates.getSingleton("PLATFORM_STATE")).thenReturn(platformReadableState); - when(platformReadableState.get()).thenReturn(platformState); - when(platformState.addressBook()) - .thenReturn(AddressBook.newBuilder() - .addresses( - Address.newBuilder() - .id(new NodeId(2L)) - .weight(111L) - .signingCertificate(getCertBytes(CERTIFICATE_2)) - // The agreementCertificate is unused, but required to prevent deserialization - // failure in - // States API. - .agreementCertificate(getCertBytes(CERTIFICATE_2)) - .hostnameInternal("10.0.55.66") - .portInternal(222) - .build(), - Address.newBuilder() - .id(new NodeId(3L)) - .weight(3L) - .signingCertificate(getCertBytes(CERTIFICATE_3)) - // The agreementCertificate is unused, but required to prevent deserialization - // failure in - // States API. - .agreementCertificate(getCertBytes(CERTIFICATE_3)) - .hostnameExternal("external3.com") - .portExternal(111) - .build()) - .build()); networkInfo.updateFrom(state); assertEquals(2, networkInfo.addressBook().size()); @@ -166,7 +134,7 @@ public void testUpdateFrom() { public void testBuildNodeInfoMapNodeNotFound() { when(nodeState.get(any(EntityNumber.class))).thenReturn(null); - StateNetworkInfo networkInfo = new StateNetworkInfo(state, activeRoster, SELF_ID, configProvider); + StateNetworkInfo networkInfo = new StateNetworkInfo(SELF_ID, state, activeRoster, configProvider); final var nodeInfo = networkInfo.nodeInfo(SELF_ID); assertNotNull(nodeInfo); diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/roster/RosterServiceTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/roster/RosterServiceTest.java index 76d72f10c300..68df0a18558b 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/roster/RosterServiceTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/roster/RosterServiceTest.java @@ -18,15 +18,16 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import com.hedera.hapi.node.state.roster.Roster; -import com.hedera.node.app.roster.schemas.V057RosterSchema; -import com.swirlds.platform.state.service.schemas.V0540RosterSchema; +import com.hedera.node.app.roster.schemas.RosterTransplantSchema; +import com.hedera.node.app.roster.schemas.V0540RosterSchema; +import com.swirlds.platform.state.service.ReadablePlatformStateStore; import com.swirlds.state.lifecycle.Schema; import com.swirlds.state.lifecycle.SchemaRegistry; import java.util.function.Predicate; +import java.util.function.Supplier; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -40,11 +41,14 @@ class RosterServiceTest { @Mock private Predicate canAdopt; + @Mock + private Supplier platformStateStoreFactory; + private RosterService rosterService; @BeforeEach void setUp() { - rosterService = new RosterService(canAdopt); + rosterService = new RosterService(canAdopt, platformStateStoreFactory); } @Test @@ -59,11 +63,11 @@ void registerExpectedSchemas() { rosterService.registerSchemas(schemaRegistry); final var captor = ArgumentCaptor.forClass(Schema.class); - verify(schemaRegistry, times(2)).register(captor.capture()); + verify(schemaRegistry).register(captor.capture()); final var schemas = captor.getAllValues(); - assertThat(schemas).hasSize(2); + assertThat(schemas).hasSize(1); assertThat(schemas.getFirst()).isInstanceOf(V0540RosterSchema.class); - assertThat(schemas.getLast()).isInstanceOf(V057RosterSchema.class); + assertThat(schemas.getFirst()).isInstanceOf(RosterTransplantSchema.class); } @Test diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/roster/schemas/V0540RosterSchemaTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/roster/schemas/V0540RosterSchemaTest.java index e676fc2fb5a2..12cfdb250b9e 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/roster/schemas/V0540RosterSchemaTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/roster/schemas/V0540RosterSchemaTest.java @@ -16,47 +16,95 @@ package com.hedera.node.app.roster.schemas; -import static com.swirlds.platform.state.service.schemas.V0540RosterSchema.ROSTER_KEY; -import static com.swirlds.platform.state.service.schemas.V0540RosterSchema.ROSTER_STATES_KEY; +import static com.hedera.node.app.fixtures.AppTestBase.DEFAULT_CONFIG; +import static com.hedera.node.app.fixtures.AppTestBase.WITH_ROSTER_LIFECYCLE; +import static com.hedera.node.app.roster.schemas.V0540RosterSchema.ROSTER_KEY; +import static com.hedera.node.app.roster.schemas.V0540RosterSchema.ROSTER_STATES_KEY; +import static com.swirlds.platform.roster.RosterRetriever.buildRoster; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.mock; +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 static org.mockito.Mockito.when; +import static org.mockito.Mockito.verifyNoMoreInteractions; -import com.hedera.hapi.node.base.SemanticVersion; +import com.hedera.hapi.node.state.roster.Roster; +import com.hedera.hapi.node.state.roster.RosterEntry; import com.hedera.hapi.node.state.roster.RosterState; -import com.hedera.node.app.spi.fixtures.util.LoggingSubject; -import com.swirlds.platform.state.service.schemas.V0540RosterSchema; +import com.hedera.node.internal.network.Network; +import com.hedera.node.internal.network.NodeMetadata; +import com.swirlds.platform.state.service.ReadablePlatformStateStore; +import com.swirlds.platform.state.service.WritableRosterStore; +import com.swirlds.platform.system.address.AddressBook; import com.swirlds.state.lifecycle.MigrationContext; +import com.swirlds.state.lifecycle.StartupNetworks; import com.swirlds.state.lifecycle.StateDefinition; import com.swirlds.state.spi.WritableSingletonState; import com.swirlds.state.spi.WritableStates; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; /** * Unit tests for {@link V0540RosterSchema}. */ +@ExtendWith(MockitoExtension.class) class V0540RosterSchemaTest { + private static final long ROUND_NO = 666L; + private static final Network NETWORK = Network.newBuilder() + .nodeMetadata(NodeMetadata.newBuilder() + .rosterEntry(RosterEntry.newBuilder().nodeId(1L).build()) + .build()) + .build(); + private static final Roster ROSTER = new Roster(NETWORK.nodeMetadata().stream() + .map(NodeMetadata::rosterEntryOrThrow) + .toList()); + private static final AddressBook ADDRESS_BOOK = new AddressBook(List.of()); - @LoggingSubject - private V0540RosterSchema subject; + @Mock + private MigrationContext ctx; + + @Mock + private WritableStates writableStates; + + @Mock + private WritableRosterStore rosterStore; + + @Mock + private StartupNetworks startupNetworks; + + @Mock + private Function rosterStoreFactory; + + @Mock + private Predicate canAdopt; + + @Mock + private Supplier platformStateStoreFactory; - private MigrationContext migrationContext; - private WritableSingletonState rosterState; + @Mock + private ReadablePlatformStateStore platformStateStore; + + @Mock + private WritableSingletonState rosterState; + + private V0540RosterSchema subject; @BeforeEach void setUp() { - subject = new V0540RosterSchema(); - migrationContext = mock(MigrationContext.class); - rosterState = mock(WritableSingletonState.class); + subject = new V0540RosterSchema(canAdopt, rosterStoreFactory, platformStateStoreFactory); } @Test - void registersExpectedRosterSchema() { + void registersExpectedSchema() { final var statesToCreate = subject.statesToCreate(); assertThat(statesToCreate).hasSize(2); final var iter = @@ -66,24 +114,135 @@ void registersExpectedRosterSchema() { } @Test - @DisplayName("For this version, migrate from existing state version returns default.") - void testMigrateFromNullRosterStateReturnsDefault() { - when(migrationContext.newStates()).thenReturn(mock(WritableStates.class)); - when(migrationContext.newStates().getSingleton(ROSTER_STATES_KEY)).thenReturn(rosterState); + void usesDefaultRosterStateIfLifecycleNotEnabled() { + given(ctx.appConfig()).willReturn(DEFAULT_CONFIG); + given(ctx.newStates()).willReturn(writableStates); + given(writableStates.getSingleton(ROSTER_STATES_KEY)).willReturn(rosterState); + + subject.migrate(ctx); - subject.migrate(migrationContext); verify(rosterState, times(1)).put(RosterState.DEFAULT); } @Test - @DisplayName("Migrate from older state version returns default.") - void testMigrateFromPreviousStateVersion() { - when(migrationContext.newStates()).thenReturn(mock(WritableStates.class)); - when(migrationContext.newStates().getSingleton(ROSTER_STATES_KEY)).thenReturn(rosterState); - when(migrationContext.previousVersion()) - .thenReturn( - SemanticVersion.newBuilder().major(0).minor(53).patch(0).build()); - subject.migrate(migrationContext); - verify(rosterState, times(1)).put(RosterState.DEFAULT); + void usesGenesisRosterIfLifecycleEnabledAndApropros() { + given(ctx.appConfig()).willReturn(WITH_ROSTER_LIFECYCLE); + given(ctx.newStates()).willReturn(writableStates); + given(ctx.isGenesis()).willReturn(true); + given(ctx.startupNetworks()).willReturn(startupNetworks); + given(startupNetworks.genesisNetworkOrThrow()).willReturn(NETWORK); + given(rosterStoreFactory.apply(writableStates)).willReturn(rosterStore); + + subject.restart(ctx); + + verify(rosterStore).putActiveRoster(ROSTER, 0L); + } + + @Test + void usesAdaptedAddressBookAndMigrationRosterIfLifecycleEnabledIfApropos() { + given(ctx.appConfig()).willReturn(WITH_ROSTER_LIFECYCLE); + given(ctx.newStates()).willReturn(writableStates); + given(ctx.startupNetworks()).willReturn(startupNetworks); + given(ctx.roundNumber()).willReturn(ROUND_NO); + given(startupNetworks.migrationNetworkOrThrow()).willReturn(NETWORK); + given(platformStateStoreFactory.get()).willReturn(platformStateStore); + given(platformStateStore.getAddressBook()).willReturn(ADDRESS_BOOK); + given(rosterStoreFactory.apply(writableStates)).willReturn(rosterStore); + + subject.restart(ctx); + + verify(rosterStore).putActiveRoster(buildRoster(ADDRESS_BOOK), 0L); + verify(rosterStore).putActiveRoster(ROSTER, ROUND_NO + 1L); + } + + @Test + void noOpIfNotUpgradeAndActiveRosterPresent() { + given(ctx.appConfig()).willReturn(WITH_ROSTER_LIFECYCLE); + given(ctx.newStates()).willReturn(writableStates); + given(ctx.startupNetworks()).willReturn(startupNetworks); + given(rosterStoreFactory.apply(writableStates)).willReturn(rosterStore); + given(rosterStore.getActiveRoster()).willReturn(ROSTER); + + subject.restart(ctx); + + verify(rosterStore).getActiveRoster(); + verifyNoMoreInteractions(rosterStore); + } + + @Test + void doesNotAdoptNullCandidateRoster() { + given(ctx.appConfig()).willReturn(WITH_ROSTER_LIFECYCLE); + given(ctx.newStates()).willReturn(writableStates); + given(ctx.startupNetworks()).willReturn(startupNetworks); + given(rosterStoreFactory.apply(writableStates)).willReturn(rosterStore); + given(rosterStore.getActiveRoster()).willReturn(ROSTER); + given(ctx.isUpgrade(any(), any())).willReturn(true); + + subject.restart(ctx); + + verify(rosterStore).getActiveRoster(); + verify(rosterStore).getCandidateRoster(); + verifyNoMoreInteractions(rosterStore); + } + + @Test + void doesNotAdoptCandidateRosterIfNotSpecified() { + given(ctx.appConfig()).willReturn(WITH_ROSTER_LIFECYCLE); + given(ctx.newStates()).willReturn(writableStates); + given(ctx.startupNetworks()).willReturn(startupNetworks); + given(rosterStoreFactory.apply(writableStates)).willReturn(rosterStore); + given(rosterStore.getActiveRoster()).willReturn(ROSTER); + given(ctx.isUpgrade(any(), any())).willReturn(true); + given(rosterStore.getCandidateRoster()).willReturn(ROSTER); + given(canAdopt.test(ROSTER)).willReturn(false); + + subject.restart(ctx); + + verify(rosterStore).getActiveRoster(); + verify(rosterStore).getCandidateRoster(); + verifyNoMoreInteractions(rosterStore); + } + + @Test + void adoptsCandidateRosterIfTestPasses() { + given(ctx.appConfig()).willReturn(WITH_ROSTER_LIFECYCLE); + given(ctx.newStates()).willReturn(writableStates); + given(ctx.startupNetworks()).willReturn(startupNetworks); + given(rosterStoreFactory.apply(writableStates)).willReturn(rosterStore); + given(rosterStore.getActiveRoster()).willReturn(ROSTER); + given(ctx.isUpgrade(any(), any())).willReturn(true); + given(rosterStore.getCandidateRoster()).willReturn(ROSTER); + given(canAdopt.test(ROSTER)).willReturn(true); + given(ctx.roundNumber()).willReturn(ROUND_NO); + + subject.restart(ctx); + + verify(rosterStore).getActiveRoster(); + verify(rosterStore).getCandidateRoster(); + verify(rosterStore).adoptCandidateRoster(ROUND_NO + 1L); + } + + @Test + void restartIsNoOpIfNotUsingLifecycle() { + given(ctx.appConfig()).willReturn(DEFAULT_CONFIG); + + subject.restart(ctx); + + verifyNoMoreInteractions(ctx); + } + + @Test + void restartSetsActiveRosterFromOverrideIfPresent() { + given(ctx.appConfig()).willReturn(WITH_ROSTER_LIFECYCLE); + given(ctx.startupNetworks()).willReturn(startupNetworks); + given(ctx.roundNumber()).willReturn(ROUND_NO); + given(ctx.newStates()).willReturn(writableStates); + given(rosterStoreFactory.apply(writableStates)).willReturn(rosterStore); + given(startupNetworks.overrideNetworkFor(ROUND_NO)).willReturn(Optional.of(NETWORK)); + + subject.restart(ctx); + + verify(rosterStore).putActiveRoster(ROSTER, ROUND_NO + 1L); + verify(startupNetworks).setOverrideRound(ROUND_NO); } } diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/roster/schemas/V057RosterSchemaTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/roster/schemas/V057RosterSchemaTest.java deleted file mode 100644 index 2ccaa2649a6a..000000000000 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/roster/schemas/V057RosterSchemaTest.java +++ /dev/null @@ -1,232 +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.roster.schemas; - -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; - -import com.hedera.hapi.node.base.SemanticVersion; -import com.hedera.hapi.node.state.roster.Roster; -import com.hedera.hapi.node.state.roster.RosterEntry; -import com.hedera.node.config.testfixtures.HederaTestConfigBuilder; -import com.hedera.node.internal.network.Network; -import com.hedera.node.internal.network.NodeMetadata; -import com.swirlds.platform.state.service.ReadablePlatformStateStore; -import com.swirlds.platform.state.service.WritableRosterStore; -import com.swirlds.platform.system.address.AddressBook; -import com.swirlds.state.lifecycle.MigrationContext; -import com.swirlds.state.lifecycle.StartupNetworks; -import com.swirlds.state.spi.WritableStates; -import edu.umd.cs.findbugs.annotations.NonNull; -import java.util.List; -import java.util.Optional; -import java.util.function.Function; -import java.util.function.Predicate; -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 V057RosterSchemaTest { - private static final long ROUND_NO = 666L; - private static final SemanticVersion THEN = - SemanticVersion.newBuilder().minor(7).build(); - private static final Network NETWORK = Network.newBuilder() - .nodeMetadata(NodeMetadata.newBuilder() - .rosterEntry(RosterEntry.newBuilder().nodeId(1L).build()) - .build()) - .build(); - private static final Roster ROSTER = new Roster(NETWORK.nodeMetadata().stream() - .map(NodeMetadata::rosterEntryOrThrow) - .toList()); - - private static final AddressBook ADDRESS_BOOK = new AddressBook(List.of()); - - @Mock - private Predicate canAdopt; - - @Mock - private WritableStates writableStates; - - @Mock - private WritableRosterStore rosterStore; - - @Mock - private MigrationContext context; - - @Mock - private StartupNetworks startupNetworks; - - @Mock - private Function rosterStoreFactory; - - @Mock - private Function platformStateStoreFactory; - - @Mock - private ReadablePlatformStateStore platformStateStore; - - private V057RosterSchema subject; - - @BeforeEach - void setUp() { - subject = new V057RosterSchema(canAdopt, rosterStoreFactory, platformStateStoreFactory); - } - - @Test - void noOpIfNotUsingRosterLifecycle() { - givenContextWith(CurrentVersion.NA, RosterLifecycle.OFF, AvailableNetwork.NONE); - - subject.restart(context); - - verify(context, never()).newStates(); - } - - @Test - void setsActiveFromStartupNetworksAtGenesis() { - givenContextWith(CurrentVersion.NA, RosterLifecycle.ON, AvailableNetwork.GENESIS); - given(context.isGenesis()).willReturn(true); - - subject.restart(context); - - verify(rosterStore).putActiveRoster(ROSTER, 0L); - } - - @Test - void doesNotAdoptCandidateIfNotUpgradeBoundary() { - givenContextWith(CurrentVersion.OLD, RosterLifecycle.ON, AvailableNetwork.NONE); - given(context.previousVersion()).willReturn(THEN); - - subject.restart(context); - - verifyNoInteractions(canAdopt); - verify(rosterStore, never()).putActiveRoster(ROSTER, ROUND_NO); - } - - @Test - void doesNotAdoptCandidateIfTestFails() { - givenContextWith(CurrentVersion.NEW, RosterLifecycle.ON, AvailableNetwork.NONE); - given(context.previousVersion()).willReturn(THEN); - given(rosterStore.getActiveRoster()).willReturn(ROSTER); - given(rosterStore.getCandidateRoster()).willReturn(ROSTER); - given(canAdopt.test(ROSTER)).willReturn(false); - - subject.restart(context); - - verify(rosterStore, never()).adoptCandidateRoster(ROUND_NO); - } - - @Test - void forcesActiveFromMigrationAtUpgradeBoundaryIfNonePresent() { - givenContextWith(CurrentVersion.NEW, RosterLifecycle.ON, AvailableNetwork.MIGRATION); - given(context.previousVersion()).willReturn(THEN); - given(context.roundNumber()).willReturn(ROUND_NO); - given(platformStateStoreFactory.apply(writableStates)).willReturn(platformStateStore); - given(platformStateStore.getAddressBook()).willReturn(ADDRESS_BOOK); - - subject.restart(context); - - verify(rosterStore).putActiveRoster(ROSTER, ROUND_NO + 1); - } - - @Test - void adoptsCandidateAtUpgradeBoundaryIfTestPasses() { - givenContextWith(CurrentVersion.NEW, RosterLifecycle.ON, AvailableNetwork.NONE); - given(context.previousVersion()).willReturn(THEN); - given(rosterStore.getActiveRoster()).willReturn(ROSTER); - given(rosterStore.getCandidateRoster()).willReturn(ROSTER); - given(canAdopt.test(ROSTER)).willReturn(true); - given(context.roundNumber()).willReturn(ROUND_NO); - - subject.restart(context); - - verify(rosterStore).adoptCandidateRoster(ROUND_NO + 1); - } - - @Test - void usesOverrideNetworkIfPresent() { - given(context.roundNumber()).willReturn(ROUND_NO); - givenContextWith(CurrentVersion.OLD, RosterLifecycle.ON, AvailableNetwork.OVERRIDE); - given(startupNetworks.overrideNetworkFor(ROUND_NO)).willReturn(Optional.of(NETWORK)); - given(platformStateStoreFactory.apply(writableStates)).willReturn(platformStateStore); - given(platformStateStore.getAddressBook()).willReturn(ADDRESS_BOOK); - - subject.restart(context); - - verify(rosterStore).putActiveRoster(ROSTER, ROUND_NO + 1); - verify(startupNetworks).setOverrideRound(ROUND_NO); - } - - private enum CurrentVersion { - NA, - OLD, - NEW, - } - - private enum RosterLifecycle { - ON, - OFF - } - - private enum AvailableNetwork { - GENESIS, - OVERRIDE, - MIGRATION, - NONE - } - - private void givenContextWith( - @NonNull final CurrentVersion currentVersion, - @NonNull final RosterLifecycle rosterLifecycle, - @NonNull final AvailableNetwork availableNetwork) { - final var configBuilder = HederaTestConfigBuilder.create() - .withValue( - "addressBook.useRosterLifecycle", - switch (rosterLifecycle) { - case ON -> "true"; - case OFF -> "false"; - }); - switch (currentVersion) { - case NA -> { - // No-op - } - case OLD -> configBuilder.withValue("hedera.services.version", "0.7.0"); - case NEW -> configBuilder.withValue("hedera.services.version", "0.42.0"); - } - given(context.configuration()).willReturn(configBuilder.getOrCreateConfig()); - if (rosterLifecycle == RosterLifecycle.ON) { - given(context.newStates()).willReturn(writableStates); - given(context.startupNetworks()).willReturn(startupNetworks); - given(rosterStoreFactory.apply(writableStates)).willReturn(rosterStore); - } - switch (availableNetwork) { - case GENESIS -> given(startupNetworks.genesisNetworkOrThrow()).willReturn(NETWORK); - case OVERRIDE -> { - given(context.roundNumber()).willReturn(ROUND_NO); - given(startupNetworks.overrideNetworkFor(ROUND_NO)).willReturn(Optional.of(NETWORK)); - } - case MIGRATION -> given(startupNetworks.migrationNetworkOrThrow()).willReturn(NETWORK); - case NONE -> { - // No-op - } - } - } -} 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 594e06f6891b..e0576d1b7c2e 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 @@ -42,7 +42,6 @@ import com.swirlds.merkledb.MerkleDb; import com.swirlds.metrics.api.Metrics; import com.swirlds.platform.config.StateConfig_; -import com.swirlds.platform.state.MerkleStateLifecycles; import com.swirlds.platform.state.PlatformMerkleStateRoot; import com.swirlds.platform.state.signed.SignedState; import com.swirlds.platform.system.InitTrigger; @@ -92,9 +91,6 @@ class SerializationTest extends MerkleTestBase { private Configuration config; private NetworkInfo networkInfo; - @Mock - private MerkleStateLifecycles lifecycles; - @Mock private MigrationStateChanges migrationStateChanges; diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/TssBaseServiceImplTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/TssBaseServiceImplTest.java index 234d5217168a..62a2a7527509 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/TssBaseServiceImplTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/TssBaseServiceImplTest.java @@ -16,34 +16,106 @@ package com.hedera.node.app.tss; +import static com.hedera.node.app.tss.RosterToKey.CANDIDATE_ROSTER; +import static com.hedera.node.app.tss.TssKeyingStatus.KEYING_COMPLETE; +import static com.hedera.node.app.tss.TssKeyingStatus.WAITING_FOR_ENCRYPTION_KEYS; +import static com.hedera.node.app.tss.TssKeyingStatus.WAITING_FOR_THRESHOLD_TSS_MESSAGES; +import static com.hedera.node.app.tss.TssKeyingStatus.WAITING_FOR_THRESHOLD_TSS_VOTES; +import static com.hedera.node.app.tss.handlers.TssUtils.SIGNATURE_SCHEMA; +import static com.hedera.node.app.workflows.handle.steps.PlatformStateUpdatesTest.ROSTER_STATE; import static com.hedera.node.app.workflows.standalone.TransactionExecutors.DEFAULT_NODE_INFO; +import static com.swirlds.platform.state.service.schemas.V0540RosterBaseSchema.ROSTER_KEY; +import static com.swirlds.platform.state.service.schemas.V0540RosterBaseSchema.ROSTER_STATES_KEY; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.verify; +import static org.mockito.internal.verification.VerificationModeFactory.times; +import com.hedera.cryptography.bls.BlsPrivateKey; +import com.hedera.cryptography.bls.BlsPublicKey; +import com.hedera.cryptography.tss.api.TssMessage; +import com.hedera.cryptography.tss.api.TssPrivateShare; +import com.hedera.cryptography.tss.api.TssPublicShare; +import com.hedera.hapi.node.state.primitives.ProtoBytes; +import com.hedera.hapi.node.state.roster.Roster; +import com.hedera.hapi.node.state.roster.RosterEntry; +import com.hedera.hapi.node.state.roster.RosterState; +import com.hedera.hapi.node.state.roster.RoundRosterPair; +import com.hedera.hapi.node.state.tss.TssEncryptionKeys; +import com.hedera.hapi.services.auxiliary.tss.TssMessageTransactionBody; +import com.hedera.hapi.services.auxiliary.tss.TssVoteTransactionBody; +import com.hedera.node.app.fixtures.state.FakeState; +import com.hedera.node.app.roster.RosterService; import com.hedera.node.app.spi.AppContext; +import com.hedera.node.app.tss.api.FakeGroupElement; +import com.hedera.node.app.tss.api.TssLibrary; +import com.hedera.node.app.tss.schemas.TssBaseTransplantSchema; import com.hedera.node.app.tss.schemas.V0560TssBaseSchema; +import com.hedera.node.app.tss.schemas.V0580TssBaseSchema; +import com.hedera.node.app.tss.stores.ReadableTssStore; import com.hedera.node.config.testfixtures.HederaTestConfigBuilder; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.swirlds.common.crypto.Signature; +import com.swirlds.common.crypto.SignatureType; +import com.swirlds.common.metrics.noop.NoOpMetrics; import com.swirlds.metrics.api.Metrics; +import com.swirlds.platform.state.service.ReadableRosterStore; +import com.swirlds.state.State; +import com.swirlds.state.lifecycle.Schema; import com.swirlds.state.lifecycle.SchemaRegistry; +import java.math.BigInteger; +import java.security.SecureRandom; import java.time.Instant; import java.time.InstantSource; import java.util.ArrayList; +import java.util.BitSet; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; +import java.util.stream.IntStream; 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.EnumSource; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) class TssBaseServiceImplTest { + private static final Bytes SOURCE_HASH = Bytes.wrap("SOURCE"); + private static final Bytes TARGET_HASH = Bytes.wrap("TARGET"); + private static final Roster SOURCE_ROSTER = Roster.newBuilder() + .rosterEntries( + new RosterEntry(0L, 4L, Bytes.EMPTY, List.of()), + new RosterEntry(1L, 3L, Bytes.EMPTY, List.of()), + new RosterEntry(2L, 2L, Bytes.EMPTY, List.of())) + .build(); + private static final Roster TARGET_ROSTER = Roster.newBuilder() + .rosterEntries( + new RosterEntry(0L, 1L, Bytes.EMPTY, List.of()), + new RosterEntry(1L, 2L, Bytes.EMPTY, List.of()), + new RosterEntry(2L, 3L, Bytes.EMPTY, List.of()), + new RosterEntry(3L, 4L, Bytes.EMPTY, List.of())) + .build(); + final BlsPublicKey FAKE_PUBLIC_KEY = + new BlsPublicKey(new FakeGroupElement(BigInteger.valueOf(10L)), SIGNATURE_SCHEMA); + final BlsPrivateKey FAKE_PRIVATE_KEY = BlsPrivateKey.create(SIGNATURE_SCHEMA, new SecureRandom()); + private static final Signature FAKE_SIGNATURE = new Signature(SignatureType.RSA, new byte[384]); + private final TssMessage TSS_MESSAGE = () -> "test".getBytes(); + private CountDownLatch latch; private final List receivedMessageHashes = new ArrayList<>(); private final List receivedSignatures = new ArrayList<>(); @@ -64,22 +136,58 @@ class TssBaseServiceImplTest { @Mock(strictness = Mock.Strictness.LENIENT) private AppContext appContext; - @Mock - private Metrics metrics; + @Mock(strictness = Mock.Strictness.LENIENT) + private ReadableRosterStore rosterStore; + + @Mock(strictness = Mock.Strictness.LENIENT) + private ReadableTssStore tssStore; + + @Mock(strictness = Mock.Strictness.LENIENT) + private TssLibrary tssLibrary; + private Metrics metrics = new NoOpMetrics(); + private State state; private TssBaseServiceImpl subject; @BeforeEach void setUp() { + final ConcurrentHashMap rosters = new ConcurrentHashMap<>(); + final AtomicReference rosterStateBackingStore = new AtomicReference<>(ROSTER_STATE); + rosterStateBackingStore.set(RosterState.newBuilder() + .roundRosterPairs(List.of(RoundRosterPair.newBuilder() + .activeRosterHash(SOURCE_HASH) + .roundNumber(1) + .build())) + .candidateRosterHash(TARGET_HASH) + .build()); + rosters.put(ProtoBytes.newBuilder().value(SOURCE_HASH).build(), SOURCE_ROSTER); + rosters.put(ProtoBytes.newBuilder().value(TARGET_HASH).build(), TARGET_ROSTER); + state = new FakeState() + .addService( + RosterService.NAME, + Map.of( + ROSTER_STATES_KEY, rosterStateBackingStore, + ROSTER_KEY, rosters)) + .addService( + TssBaseService.NAME, + Map.of( + V0580TssBaseSchema.TSS_ENCRYPTION_KEYS_KEY, + new HashMap<>(), + V0560TssBaseSchema.TSS_MESSAGE_MAP_KEY, + new HashMap<>(), + V0560TssBaseSchema.TSS_VOTE_MAP_KEY, + new HashMap<>())); + given(appContext.gossip()).willReturn(gossip); given(appContext.instantSource()).willReturn(InstantSource.system()); given(appContext.configSupplier()).willReturn(HederaTestConfigBuilder::createConfig); given(appContext.selfNodeInfoSupplier()).willReturn(() -> DEFAULT_NODE_INFO); + subject = new TssBaseServiceImpl( appContext, ForkJoinPool.commonPool(), ForkJoinPool.commonPool(), - new TssLibraryImpl(appContext), + tssLibrary, ForkJoinPool.commonPool(), metrics); } @@ -109,7 +217,325 @@ void onlyRegisteredConsumerReceiveCallbacks() throws InterruptedException { @Test void placeholderRegistersSchemas() { + final var captor = ArgumentCaptor.forClass(Schema.class); + subject.registerSchemas(registry); - verify(registry).register(argThat(s -> s instanceof V0560TssBaseSchema)); + + verify(registry, times(2)).register(captor.capture()); + final var schemas = captor.getAllValues(); + assertThat(schemas.getFirst()).isInstanceOf(V0560TssBaseSchema.class); + assertThat(schemas.getLast()).isInstanceOf(V0580TssBaseSchema.class); + assertThat(schemas.getLast()).isInstanceOf(TssBaseTransplantSchema.class); + } + + @Test + void managesTssStatusWhenRosterToKeyIsNone() { + final var oldStatus = new TssStatus(WAITING_FOR_THRESHOLD_TSS_MESSAGES, RosterToKey.NONE, Bytes.EMPTY); + final var expectedTssStatus = new TssStatus(WAITING_FOR_ENCRYPTION_KEYS, CANDIDATE_ROSTER, Bytes.EMPTY); + subject.setTssStatus(oldStatus); + subject.updateTssStatus( + true, + Instant.ofEpochSecond(1_234_567L), + new TssBaseServiceImpl.RosterAndTssInfo( + SOURCE_ROSTER, + SOURCE_HASH, + SOURCE_ROSTER, + SOURCE_HASH, + List.of(), + Optional.empty(), + List.of(), + null)); + assertEquals(expectedTssStatus, subject.getTssStatus()); + } + + @ParameterizedTest + @EnumSource( + value = RosterToKey.class, + names = {"ACTIVE_ROSTER", "CANDIDATE_ROSTER"}) + void managesTssStatusWhenMessagesReachedThreshold(RosterToKey rosterToKey) { + final var messages = IntStream.range(0, 8) + .mapToObj(i -> + new TssMessageTransactionBody(SOURCE_HASH, TARGET_HASH, i * 2L + 1, Bytes.wrap("MESSAGE" + i))) + .toList(); + final var oldStatus = new TssStatus(WAITING_FOR_THRESHOLD_TSS_MESSAGES, rosterToKey, Bytes.EMPTY); + final var expectedTssStatus = new TssStatus(WAITING_FOR_THRESHOLD_TSS_VOTES, rosterToKey, Bytes.EMPTY); + subject.setTssStatus(oldStatus); + + given(rosterStore.getCurrentRosterHash()).willReturn(SOURCE_HASH); + given(tssLibrary.verifyTssMessage(any(), any())).willReturn(true); + + subject.ensureParticipantDirectoryKnown(state); + subject.updateTssStatus( + true, + Instant.ofEpochSecond(1_234_567L), + new TssBaseServiceImpl.RosterAndTssInfo( + SOURCE_ROSTER, + SOURCE_HASH, + SOURCE_ROSTER, + SOURCE_HASH, + messages, + Optional.empty(), + List.of(), + null)); + assertEquals(expectedTssStatus, subject.getTssStatus()); + } + + @ParameterizedTest + @EnumSource( + value = RosterToKey.class, + names = {"ACTIVE_ROSTER", "CANDIDATE_ROSTER"}) + void submitsTssMessageIfSelfHasNotSubmitted(RosterToKey rosterToKey) { + final var messages = IntStream.range(0, 4) + .mapToObj(i -> + new TssMessageTransactionBody(SOURCE_HASH, TARGET_HASH, i * 2L + 1, Bytes.wrap("MESSAGE" + i))) + .toList(); + final var oldStatus = new TssStatus(WAITING_FOR_THRESHOLD_TSS_MESSAGES, rosterToKey, Bytes.EMPTY); + final var expectedTssStatus = new TssStatus(WAITING_FOR_THRESHOLD_TSS_MESSAGES, rosterToKey, Bytes.EMPTY); + subject.setTssStatus(oldStatus); + + given(rosterStore.getCurrentRosterHash()).willReturn(SOURCE_HASH); + given(tssLibrary.verifyTssMessage(any(), any())).willReturn(true); + given(tssLibrary.generateTssMessage(any())).willReturn(TSS_MESSAGE); + given(tssLibrary.generateTssMessage(any(), any())).willReturn(TSS_MESSAGE); + given(tssLibrary.computePublicShares(any(), any())).willReturn(List.of(new TssPublicShare(1, FAKE_PUBLIC_KEY))); + given(tssLibrary.decryptPrivateShares(any(), any())) + .willReturn(List.of(new TssPrivateShare(1, FAKE_PRIVATE_KEY))); + assertFalse(subject.haveSentMessageForTargetRoster()); + given(tssStore.getMessagesForTarget(any())).willReturn(messages); + + subject.ensureParticipantDirectoryKnown(state); + subject.getTssKeysAccessor().generateKeyMaterialForActiveRoster(state); + subject.updateTssStatus( + true, + Instant.ofEpochSecond(1_234_567L), + new TssBaseServiceImpl.RosterAndTssInfo( + SOURCE_ROSTER, + SOURCE_HASH, + SOURCE_ROSTER, + SOURCE_HASH, + messages, + Optional.empty(), + List.of(), + null)); + if (rosterToKey == RosterToKey.ACTIVE_ROSTER) { + verify(tssLibrary).generateTssMessage(any()); + } else { + verify(tssLibrary).generateTssMessage(any(), any()); + } + assertEquals(expectedTssStatus, subject.getTssStatus()); + assertTrue(subject.haveSentMessageForTargetRoster()); + } + + @ParameterizedTest + @EnumSource( + value = RosterToKey.class, + names = {"ACTIVE_ROSTER", "CANDIDATE_ROSTER"}) + void managesTssStatusWhenKeyingComplete(RosterToKey rosterToKey) { + final var oldStatus = new TssStatus(KEYING_COMPLETE, rosterToKey, Bytes.EMPTY); + final var expectedTssStatus = new TssStatus(KEYING_COMPLETE, RosterToKey.NONE, Bytes.EMPTY); + subject.setTssStatus(oldStatus); + + given(rosterStore.getCurrentRosterHash()).willReturn(SOURCE_HASH); + given(rosterStore.getCandidateRoster()).willReturn(TARGET_ROSTER); + + subject.ensureParticipantDirectoryKnown(state); + subject.updateTssStatus( + true, + Instant.ofEpochSecond(1_234_567L), + new TssBaseServiceImpl.RosterAndTssInfo( + SOURCE_ROSTER, + SOURCE_HASH, + SOURCE_ROSTER, + SOURCE_HASH, + List.of(), + Optional.empty(), + List.of(), + null)); + assertEquals(expectedTssStatus, subject.getTssStatus()); + } + + @ParameterizedTest + @EnumSource( + value = RosterToKey.class, + names = {"ACTIVE_ROSTER", "CANDIDATE_ROSTER"}) + void managesTssStatusWhenVotesReachedThreshold(RosterToKey rosterToKey) { + final var oldStatus = new TssStatus(WAITING_FOR_THRESHOLD_TSS_VOTES, rosterToKey, Bytes.EMPTY); + final var expectedTssStatus = new TssStatus(KEYING_COMPLETE, rosterToKey, Bytes.wrap("ledger")); + subject.setTssStatus(oldStatus); + + given(rosterStore.getCurrentRosterHash()).willReturn(SOURCE_HASH); + given(rosterStore.getCandidateRoster()).willReturn(TARGET_ROSTER); + + subject.ensureParticipantDirectoryKnown(state); + subject.updateTssStatus( + true, + Instant.ofEpochSecond(1_234_567L), + new TssBaseServiceImpl.RosterAndTssInfo( + SOURCE_ROSTER, + SOURCE_HASH, + TARGET_ROSTER, + TARGET_HASH, + List.of(), + Optional.of(TssVoteTransactionBody.newBuilder() + .ledgerId(Bytes.wrap("ledger")) + .tssVote(Bytes.wrap( + BitSet.valueOf(new long[] {1L, 2L}).toByteArray())) + .build()), + List.of(), + null)); + assertEquals(expectedTssStatus, subject.getTssStatus()); + } + + @ParameterizedTest + @EnumSource( + value = RosterToKey.class, + names = {"ACTIVE_ROSTER", "CANDIDATE_ROSTER"}) + void submitsVoteWhenSelfHasNotSubmittedVotes(RosterToKey rosterToKey) { + final var messages = IntStream.range(0, 8) + .mapToObj(i -> + new TssMessageTransactionBody(SOURCE_HASH, TARGET_HASH, i * 2L + 1, Bytes.wrap("MESSAGE" + i))) + .toList(); + + final var oldStatus = new TssStatus(WAITING_FOR_THRESHOLD_TSS_VOTES, rosterToKey, Bytes.EMPTY); + final var expectedTssStatus = new TssStatus(WAITING_FOR_THRESHOLD_TSS_VOTES, rosterToKey, Bytes.EMPTY); + subject.setTssStatus(oldStatus); + + given(rosterStore.getCurrentRosterHash()).willReturn(SOURCE_HASH); + given(rosterStore.getCandidateRoster()).willReturn(TARGET_ROSTER); + given(tssLibrary.computePublicShares(any(), any())).willReturn(List.of(new TssPublicShare(1, FAKE_PUBLIC_KEY))); + given(tssLibrary.aggregatePublicShares(any())).willReturn(FAKE_PUBLIC_KEY); + given(tssLibrary.verifyTssMessage(any(), any())).willReturn(true); + given(gossip.sign(any())).willReturn(FAKE_SIGNATURE); + + subject.ensureParticipantDirectoryKnown(state); + subject.updateTssStatus( + true, + Instant.ofEpochSecond(1_234_567L), + new TssBaseServiceImpl.RosterAndTssInfo( + SOURCE_ROSTER, + SOURCE_HASH, + TARGET_ROSTER, + TARGET_HASH, + messages, + Optional.empty(), + List.of(), + null)); + assertEquals(expectedTssStatus, subject.getTssStatus()); + assertTrue(subject.haveSentVoteForTargetRoster()); + } + + @Test + void managesTssStatusOnIntializationWaitingForkeys() { + final var expectedTssStatus = + new TssStatus(WAITING_FOR_ENCRYPTION_KEYS, RosterToKey.ACTIVE_ROSTER, Bytes.EMPTY); + + given(rosterStore.getCurrentRosterHash()).willReturn(SOURCE_HASH); + given(rosterStore.getActiveRoster()).willReturn(SOURCE_ROSTER); + given(rosterStore.getCandidateRoster()).willReturn(null); + given(tssLibrary.computePublicShares(any(), any())).willReturn(List.of(new TssPublicShare(1, FAKE_PUBLIC_KEY))); + given(tssLibrary.aggregatePublicShares(any())).willReturn(FAKE_PUBLIC_KEY); + given(tssLibrary.verifyTssMessage(any(), any())).willReturn(true); + + subject.ensureParticipantDirectoryKnown(state); + final var tssStatus = subject.computeInitialTssStatus(tssStore, rosterStore); + assertEquals(expectedTssStatus, tssStatus); + } + + @Test + void managesTssStatusOnIntializationWaitingForMessages() { + final var expectedTssStatus = + new TssStatus(WAITING_FOR_THRESHOLD_TSS_MESSAGES, RosterToKey.ACTIVE_ROSTER, Bytes.EMPTY); + + given(rosterStore.getCurrentRosterHash()).willReturn(SOURCE_HASH); + given(rosterStore.getActiveRoster()).willReturn(SOURCE_ROSTER); + given(rosterStore.getCandidateRoster()).willReturn(null); + given(tssLibrary.computePublicShares(any(), any())).willReturn(List.of(new TssPublicShare(1, FAKE_PUBLIC_KEY))); + given(tssLibrary.aggregatePublicShares(any())).willReturn(FAKE_PUBLIC_KEY); + given(tssLibrary.verifyTssMessage(any(), any())).willReturn(true); + given(tssStore.getTssEncryptionKeys(anyLong())) + .willReturn(TssEncryptionKeys.newBuilder() + .currentEncryptionKey(Bytes.wrap("test")) + .build()); + + subject.ensureParticipantDirectoryKnown(state); + final var tssStatus = subject.computeInitialTssStatus(tssStore, rosterStore); + assertEquals(expectedTssStatus, tssStatus); + } + + @Test + void managesTssStatusOnIntializationWaitingForVotes() { + final var messages = IntStream.range(0, 8) + .mapToObj(i -> + new TssMessageTransactionBody(SOURCE_HASH, TARGET_HASH, i * 2L + 1, Bytes.wrap("MESSAGE" + i))) + .toList(); + + final var expectedTssStatus = + new TssStatus(WAITING_FOR_THRESHOLD_TSS_VOTES, RosterToKey.ACTIVE_ROSTER, Bytes.EMPTY); + + given(rosterStore.getCurrentRosterHash()).willReturn(SOURCE_HASH); + given(rosterStore.getActiveRoster()).willReturn(SOURCE_ROSTER); + given(rosterStore.getCandidateRoster()).willReturn(null); + given(tssLibrary.computePublicShares(any(), any())).willReturn(List.of(new TssPublicShare(1, FAKE_PUBLIC_KEY))); + given(tssLibrary.aggregatePublicShares(any())).willReturn(FAKE_PUBLIC_KEY); + given(tssLibrary.verifyTssMessage(any(), any())).willReturn(true); + given(tssStore.getTssEncryptionKeys(anyLong())) + .willReturn(TssEncryptionKeys.newBuilder() + .currentEncryptionKey(Bytes.wrap("test")) + .build()); + given(tssStore.getMessagesForTarget(any())).willReturn(messages); + + subject.ensureParticipantDirectoryKnown(state); + final var tssStatus = subject.computeInitialTssStatus(tssStore, rosterStore); + assertEquals(expectedTssStatus, tssStatus); + } + + @Test + void managesTssStatusOnIntializationWaitingForMessagesCandidateRoster() { + final var expectedTssStatus = new TssStatus(WAITING_FOR_THRESHOLD_TSS_MESSAGES, CANDIDATE_ROSTER, Bytes.EMPTY); + + given(rosterStore.getCurrentRosterHash()).willReturn(SOURCE_HASH); + given(rosterStore.getActiveRoster()).willReturn(SOURCE_ROSTER); + given(rosterStore.getCandidateRoster()).willReturn(TARGET_ROSTER); + given(tssLibrary.computePublicShares(any(), any())).willReturn(List.of(new TssPublicShare(1, FAKE_PUBLIC_KEY))); + given(tssLibrary.aggregatePublicShares(any())).willReturn(FAKE_PUBLIC_KEY); + given(tssLibrary.verifyTssMessage(any(), any())).willReturn(true); + given(tssStore.getTssEncryptionKeys(anyLong())) + .willReturn(TssEncryptionKeys.newBuilder() + .currentEncryptionKey(Bytes.wrap("test")) + .build()); + given(tssStore.getMessage(any())) + .willReturn(new TssMessageTransactionBody(SOURCE_HASH, TARGET_HASH, 1L, Bytes.EMPTY)); + given(tssStore.anyWinningVoteFor(SOURCE_HASH, rosterStore)) + .willReturn(Optional.of(TssVoteTransactionBody.DEFAULT)); + + subject.ensureParticipantDirectoryKnown(state); + + final var tssStatus = subject.computeInitialTssStatus(tssStore, rosterStore); + assertEquals(expectedTssStatus, tssStatus); + } + + @Test + void managesTssStatusOnIntializationWaitingForVotesCandidateRoster() { + final var expectedTssStatus = new TssStatus(WAITING_FOR_THRESHOLD_TSS_MESSAGES, CANDIDATE_ROSTER, Bytes.EMPTY); + + given(rosterStore.getCurrentRosterHash()).willReturn(SOURCE_HASH); + given(rosterStore.getActiveRoster()).willReturn(SOURCE_ROSTER); + given(rosterStore.getCandidateRoster()).willReturn(TARGET_ROSTER); + given(tssLibrary.computePublicShares(any(), any())).willReturn(List.of(new TssPublicShare(1, FAKE_PUBLIC_KEY))); + given(tssLibrary.aggregatePublicShares(any())).willReturn(FAKE_PUBLIC_KEY); + given(tssLibrary.verifyTssMessage(any(), any())).willReturn(true); + given(tssStore.getTssEncryptionKeys(anyLong())) + .willReturn(TssEncryptionKeys.newBuilder() + .currentEncryptionKey(Bytes.wrap("test")) + .build()); + given(tssStore.getMessage(any())) + .willReturn(new TssMessageTransactionBody(SOURCE_HASH, TARGET_HASH, 1L, Bytes.EMPTY)); + given(tssStore.anyWinningVoteFor(SOURCE_HASH, rosterStore)) + .willReturn(Optional.of(TssVoteTransactionBody.DEFAULT)); + + subject.ensureParticipantDirectoryKnown(state); + + final var tssStatus = subject.computeInitialTssStatus(tssStore, rosterStore); + assertEquals(expectedTssStatus, tssStatus); } } diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/TssBaseServiceTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/TssBaseServiceTest.java index 17f039d7b3a0..49ed562f373c 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/TssBaseServiceTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/TssBaseServiceTest.java @@ -34,6 +34,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import com.hedera.cryptography.tss.api.TssMessage; import com.hedera.cryptography.tss.api.TssParticipantDirectory; import com.hedera.cryptography.tss.api.TssPrivateShare; import com.hedera.cryptography.tss.api.TssPublicShare; @@ -90,8 +91,7 @@ public class TssBaseServiceTest { @Mock(strictness = Mock.Strictness.LENIENT) private NetworkInfo networkInfo; - @Mock - private com.hedera.cryptography.tss.api.TssMessage tssMessage; + private final TssMessage TSS_MESSAGE = "test"::getBytes; @Mock private Executor executor; @@ -145,6 +145,7 @@ void doesntSetSameCandidateRoster() { // Simulate CURRENT_CANDIDATE_ROSTER and ACTIVE_ROSTER mockWritableRosterStore(); given(tssLibrary.decryptPrivateShares(any(), any())).willReturn(List.of()); + given(tssLibrary.generateTssMessage(any(), any())).willReturn(TSS_MESSAGE); // Attempt to set the same candidate roster subject.setCandidateRoster(CURRENT_CANDIDATE_ROSTER, handleContext); @@ -161,7 +162,7 @@ void doesntSetActiveRosterAsCandidateRoster() { given(handleContext.storeFactory()).willReturn(storeFactory); given(storeFactory.writableStore(WritableRosterStore.class)).willReturn(rosterStore); given(tssLibrary.decryptPrivateShares(any(), any())).willReturn(List.of()); - given(tssLibrary.generateTssMessage(any(), any())).willReturn(tssMessage); + given(tssLibrary.generateTssMessage(any(), any())).willReturn(TSS_MESSAGE); given(handleContext.consensusNow()).willReturn(Instant.ofEpochSecond(1_234_567L)); subject.setCandidateRoster(ACTIVE_ROSTER, handleContext); @@ -180,7 +181,7 @@ void setsCandidateRoster() { // Simulate the _current_ candidate roster and active roster final var rosterStore = mockWritableRosterStore(); given(tssLibrary.decryptPrivateShares(any(), any())).willReturn(List.of()); - given(tssLibrary.generateTssMessage(any(), any())).willReturn(tssMessage); + given(tssLibrary.generateTssMessage(any(), any())).willReturn(TSS_MESSAGE); given(handleContext.consensusNow()).willReturn(Instant.ofEpochSecond(1_234_567L)); final var inputRoster = Roster.newBuilder() .rosterEntries(List.of(ROSTER_NODE_1, ROSTER_NODE_2, ROSTER_NODE_3)) diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/TssCryptographyManagerTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/TssCryptographyManagerTest.java index 7c5b78987583..b08e3ce9361f 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/TssCryptographyManagerTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/TssCryptographyManagerTest.java @@ -38,7 +38,6 @@ import com.hedera.node.app.spi.store.StoreFactory; import com.hedera.node.app.spi.workflows.HandleContext; import com.hedera.node.app.tss.api.TssLibrary; -import com.hedera.node.app.tss.stores.WritableTssStore; import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.common.crypto.Signature; import com.swirlds.state.lifecycle.info.NetworkInfo; @@ -74,9 +73,6 @@ public class TssCryptographyManagerTest { @Mock private StoreFactory storeFactory; - @Mock - private WritableTssStore tssStore; - @Mock private TssMetrics tssMetrics; @@ -96,10 +92,9 @@ void setUp() { void testWhenVoteAlreadySubmitted() { final var body = getTssBody(); when(handleContext.storeFactory()).thenReturn(storeFactory); - when(storeFactory.writableStore(WritableTssStore.class)).thenReturn(tssStore); - when(tssStore.getVote(any())).thenReturn(mock(TssVoteTransactionBody.class)); // Simulate vote already submitted - - final var result = subject.getVoteFuture(body.targetRosterHash(), tssParticipantDirectory, handleContext); + // Simulate vote already submitted + final var result = + subject.getVoteFuture(tssParticipantDirectory, List.of(body), (mock(TssVoteTransactionBody.class))); assertNull(result.join()); } @@ -108,10 +103,7 @@ void testWhenVoteAlreadySubmitted() { void testWhenVoteNoVoteSubmittedAndThresholdNotMet() { final var body = getTssBody(); when(handleContext.storeFactory()).thenReturn(storeFactory); - when(storeFactory.writableStore(WritableTssStore.class)).thenReturn(tssStore); - when(tssStore.getVote(any())).thenReturn(null); - - final var result = subject.getVoteFuture(body.targetRosterHash(), tssParticipantDirectory, handleContext); + final var result = subject.getVoteFuture(tssParticipantDirectory, List.of(body), null); assertNull(result.join()); } @@ -124,17 +116,13 @@ void testWhenVoteNoVoteSubmittedAndThresholdMet() { final var body = getTssBody(); when(handleContext.storeFactory()).thenReturn(storeFactory); - when(storeFactory.writableStore(WritableTssStore.class)).thenReturn(tssStore); - when(tssStore.getVote(any())).thenReturn(null); - when(tssStore.getMessagesForTarget(any())).thenReturn(List.of(body)); when(tssLibrary.verifyTssMessage(any(), any())).thenReturn(true); when(tssLibrary.computePublicShares(any(), any())).thenReturn(mockPublicShares); when(tssLibrary.aggregatePublicShares(any())).thenReturn(ledgerId); when(gossip.sign(any())).thenReturn(mockSignature); - final var result = subject.getVoteFuture(body.targetRosterHash(), tssParticipantDirectory, handleContext); - + final var result = subject.getVoteFuture(tssParticipantDirectory, List.of(body), null); assertNotNull(result.join()); verify(gossip).sign(ledgerId.toBytes()); } @@ -143,14 +131,10 @@ void testWhenVoteNoVoteSubmittedAndThresholdMet() { void testWhenMetException() { final var body = getTssBody(); when(handleContext.storeFactory()).thenReturn(storeFactory); - when(storeFactory.writableStore(WritableTssStore.class)).thenReturn(tssStore); - when(tssStore.getVote(any())).thenReturn(null); - when(tssStore.getMessagesForTarget(any())).thenReturn(List.of(body)); when(tssLibrary.verifyTssMessage(any(), any())).thenReturn(true); when(tssLibrary.computePublicShares(any(), any())).thenThrow(new RuntimeException()); - - final var result = subject.getVoteFuture(body.targetRosterHash(), tssParticipantDirectory, handleContext); + final var result = subject.getVoteFuture(tssParticipantDirectory, List.of(body), null); assertNull(result.join()); verify(gossip, never()).sign(any()); 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 index b9dcf944055e..f5c09afaba6b 100644 --- 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 @@ -31,6 +31,7 @@ import com.hedera.hapi.node.base.AccountID; 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.store.StoreFactory; import com.hedera.node.app.spi.workflows.HandleContext; import com.hedera.node.app.spi.workflows.PreHandleContext; @@ -131,13 +132,10 @@ void submitsVoteOnHandlingMessageWhenThresholdMet() { when(handleContext.storeFactory()).thenReturn(storeFactory); when(storeFactory.writableStore(WritableTssStore.class)).thenReturn(tssStore); - given(tssCryptographyManager.getVoteFuture( - eq(getTssBody().tssMessageOrThrow().targetRosterHash()), - any(TssParticipantDirectory.class), - eq(handleContext))) + given(tssCryptographyManager.getVoteFuture(any(TssParticipantDirectory.class), any(), any())) .willReturn(CompletableFuture.completedFuture(vote)); given(signature.getBytes()).willReturn(Bytes.wrap("test")); - given(directoryAccessor.activeParticipantDirectory()).willReturn(TSS_KEYS.activeParticipantDirectory()); + given(directoryAccessor.activeParticipantDirectoryOrThrow()).willReturn(TSS_KEYS.activeParticipantDirectory()); given(pairingPublicKey.toBytes()).willReturn("test".getBytes()); subject.handle(handleContext); @@ -147,12 +145,12 @@ void submitsVoteOnHandlingMessageWhenThresholdMet() { @Test public void testHandleException() { when(handleContext.body()).thenReturn(getTssBody()); - when(tssCryptographyManager.getVoteFuture(any(), any(), any())) + when(tssCryptographyManager.getVoteFuture(any(TssParticipantDirectory.class), any(), any())) .thenThrow(new RuntimeException("Simulated error")); // Execute the handler and ensure no vote is submitted assertThrows(RuntimeException.class, () -> subject.handle(handleContext)); - verify(submissionManager, never()).submitTssVote(any(), any()); + verify(submissionManager, never()).submitTssVote(any(TssVoteTransactionBody.class), any(HandleContext.class)); } public static TransactionBody getTssBody() { 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 index e2ca0ee2f8ad..d5da697204e8 100644 --- 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 @@ -90,10 +90,9 @@ void setUp() { given(appContext.gossip()).willReturn(gossip); subject = new TssSubmissions(appContext, 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); + given(appContext.configSupplier()).willReturn(() -> TEST_CONFIG); + given(appContext.selfNodeInfoSupplier()).willReturn(() -> nodeInfo); } @Test diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/handlers/TssUtilsTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/handlers/TssUtilsTest.java index 0af0f618afde..2bd23e77a4ec 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/handlers/TssUtilsTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/handlers/TssUtilsTest.java @@ -17,7 +17,8 @@ package com.hedera.node.app.tss.handlers; import static com.hedera.node.app.tss.handlers.TssMessageHandlerTest.getTssBody; -import static com.hedera.node.app.tss.handlers.TssUtils.validateTssMessages; +import static com.hedera.node.app.tss.handlers.TssUtils.SIGNATURE_SCHEMA; +import static com.hedera.node.app.tss.handlers.TssUtils.voteForValidMessages; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -26,25 +27,30 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; +import com.hedera.cryptography.bls.BlsPublicKey; import com.hedera.cryptography.tss.api.TssMessage; import com.hedera.cryptography.tss.api.TssParticipantDirectory; import com.hedera.hapi.node.state.roster.Roster; import com.hedera.hapi.node.state.roster.RosterEntry; +import com.hedera.node.app.tss.api.FakeGroupElement; import com.hedera.node.app.tss.api.TssLibrary; import com.hedera.pbj.runtime.io.buffer.Bytes; +import java.math.BigInteger; import java.util.List; import org.junit.jupiter.api.Test; public class TssUtilsTest { + private static final BlsPublicKey FAKE_ENCRYPTION_KEY = + new BlsPublicKey(new FakeGroupElement(BigInteger.valueOf(123L)), SIGNATURE_SCHEMA); + @Test public void testComputeParticipantDirectory() { RosterEntry rosterEntry1 = new RosterEntry(1L, 100L, null, null); RosterEntry rosterEntry2 = new RosterEntry(2L, 50L, null, null); - long maxSharesPerNode = 10L; - int selfNodeId = 1; + int maxSharesPerNode = 10; - TssParticipantDirectory directory = - TssUtils.computeParticipantDirectory(new Roster(List.of(rosterEntry1, rosterEntry2)), maxSharesPerNode); + TssParticipantDirectory directory = TssUtils.computeParticipantDirectory( + new Roster(List.of(rosterEntry1, rosterEntry2)), maxSharesPerNode, nodeId -> FAKE_ENCRYPTION_KEY); assertNotNull(directory); assertEquals((15 + 2) / 2, directory.getThreshold()); @@ -53,29 +59,32 @@ public void testComputeParticipantDirectory() { } @Test - public void testValidateTssMessages() { + public void testVoteForValidMessages() { final var body = getTssBody(); final var tssLibrary = mock(TssLibrary.class); final var tssParticipantDirectory = mock(TssParticipantDirectory.class); given(tssLibrary.verifyTssMessage(any(), any())).willReturn(true); - final var validMessages = - validateTssMessages(List.of(body.tssMessageOrThrow()), tssParticipantDirectory, tssLibrary); + final var validMessages = voteForValidMessages( + List.of(body.tssMessageOrThrow()), tssParticipantDirectory, tssLibrary) + .get() + .validTssMessages(); assertEquals(1, validMessages.size()); } @Test - public void testValidateTssMessagesFails() { + public void testVoteForValidMessagesFails() { final var body = getTssBody(); final var tssLibrary = mock(TssLibrary.class); final var tssParticipantDirectory = mock(TssParticipantDirectory.class); given(tssLibrary.verifyTssMessage(any(), any())).willReturn(false); + given(tssParticipantDirectory.getShareIds()).willReturn(List.of(1, 2, 3, 4)); - final var validMessages = - validateTssMessages(List.of(body.tssMessageOrThrow()), tssParticipantDirectory, tssLibrary); + final var validMessagesForVote = + voteForValidMessages(List.of(body.tssMessageOrThrow()), tssParticipantDirectory, tssLibrary); - assertEquals(0, validMessages.size()); + assertTrue(validMessagesForVote.isEmpty()); } @Test @@ -84,14 +93,13 @@ public void testGetTssMessages() { final var tssMessage = mock(TssMessage.class); RosterEntry rosterEntry1 = new RosterEntry(1L, 100L, null, null); RosterEntry rosterEntry2 = new RosterEntry(2L, 50L, null, null); - long maxSharesPerNode = 10L; - int selfNodeId = 1; + int maxSharesPerNode = 10; given(library.getTssMessageFromBytes(any(), any())).willReturn(tssMessage); given(tssMessage.toBytes()) .willReturn(Bytes.wrap("tssMessage".getBytes()).toByteArray()); - TssParticipantDirectory directory = - TssUtils.computeParticipantDirectory(new Roster(List.of(rosterEntry1, rosterEntry2)), maxSharesPerNode); + TssParticipantDirectory directory = TssUtils.computeParticipantDirectory( + new Roster(List.of(rosterEntry1, rosterEntry2)), maxSharesPerNode, nodeId -> FAKE_ENCRYPTION_KEY); final var body = getTssBody(); final var validTssOps = List.of(body.tssMessageOrThrow()); diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/schemas/TssBaseTransplantSchemaTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/schemas/TssBaseTransplantSchemaTest.java new file mode 100644 index 000000000000..e0ed2cff734f --- /dev/null +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/schemas/TssBaseTransplantSchemaTest.java @@ -0,0 +1,175 @@ +/* + * 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.schemas; + +import static com.hedera.node.app.tss.schemas.V0580TssBaseSchema.TSS_ENCRYPTION_KEYS_KEY; +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.verifyNoMoreInteractions; + +import com.hedera.hapi.node.state.common.EntityNumber; +import com.hedera.hapi.node.state.roster.RosterEntry; +import com.hedera.hapi.node.state.tss.TssEncryptionKeys; +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.hedera.node.config.testfixtures.HederaTestConfigBuilder; +import com.hedera.node.internal.network.Network; +import com.hedera.node.internal.network.NodeMetadata; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.swirlds.platform.roster.RosterUtils; +import com.swirlds.state.lifecycle.MigrationContext; +import com.swirlds.state.lifecycle.StartupNetworks; +import com.swirlds.state.spi.WritableKVState; +import com.swirlds.state.spi.WritableStates; +import java.util.List; +import java.util.Optional; +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 TssBaseTransplantSchemaTest { + private static final long ROUND_NO = 666L; + private static final Bytes FAKE_LEDGER_ID = Bytes.fromHex("abcd"); + private static final Bytes NODE1_ENCRYPTION_KEY = Bytes.wrap("NODE1_ENCRYPTION_KEY"); + private static final Bytes NODE2_ENCRYPTION_KEY = Bytes.wrap("NODE2_ENCRYPTION_KEY"); + private static final Network NETWORK_WITH_KEYS = Network.newBuilder() + .nodeMetadata( + NodeMetadata.newBuilder() + .rosterEntry(RosterEntry.newBuilder().nodeId(1L).build()) + .tssEncryptionKey(NODE1_ENCRYPTION_KEY) + .build(), + NodeMetadata.newBuilder() + .rosterEntry(RosterEntry.newBuilder().nodeId(2L).build()) + .tssEncryptionKey(NODE2_ENCRYPTION_KEY) + .build()) + .build(); + private static final Network NETWORK_WITHOUT_KEYS = NETWORK_WITH_KEYS + .copyBuilder() + .nodeMetadata(NETWORK_WITH_KEYS.nodeMetadata().stream() + .map(nm -> nm.copyBuilder().tssEncryptionKey(Bytes.EMPTY).build()) + .toList()) + .build(); + private static Network NETWORK_WITH_TSS_KEY_MATERIAL = NETWORK_WITH_KEYS + .copyBuilder() + .ledgerId(FAKE_LEDGER_ID) + .tssMessages(List.of( + new TssMessageTransactionBody(Bytes.EMPTY, Bytes.EMPTY, 1L, Bytes.EMPTY), + new TssMessageTransactionBody(Bytes.EMPTY, Bytes.EMPTY, 2L, Bytes.EMPTY))) + .build(); + + @Mock + private MigrationContext ctx; + + @Mock + private StartupNetworks startupNetworks; + + @Mock + private WritableStates writableStates; + + @Mock + private WritableKVState writableEncryptionKeys; + + @Mock + private WritableKVState writableVotes; + + @Mock + private WritableKVState writableMessages; + + private final TssBaseTransplantSchema subject = new V0580TssBaseSchema(); + + @Test + void noOpWithoutTssEnabled() { + givenConfig(false); + subject.restart(ctx); + verifyNoMoreInteractions(ctx); + } + + @Test + void notGenesisAndNoOverridePresentIsNoop() { + givenConfig(true); + given(ctx.roundNumber()).willReturn(ROUND_NO); + given(ctx.startupNetworks()).willReturn(startupNetworks); + + subject.restart(ctx); + + verify(startupNetworks).overrideNetworkFor(ROUND_NO); + } + + @Test + void withOverrideSetsEncryptionKeysFromNetwork() { + givenConfig(true); + given(ctx.roundNumber()).willReturn(ROUND_NO); + given(ctx.startupNetworks()).willReturn(startupNetworks); + given(startupNetworks.overrideNetworkFor(ROUND_NO)).willReturn(Optional.of(NETWORK_WITH_KEYS)); + given(ctx.newStates()).willReturn(writableStates); + given(writableStates.get(TSS_ENCRYPTION_KEYS_KEY)) + .willReturn(writableEncryptionKeys); + + subject.restart(ctx); + + verify(writableEncryptionKeys) + .put(new EntityNumber(1L), new TssEncryptionKeys(NODE1_ENCRYPTION_KEY, Bytes.EMPTY)); + verify(writableEncryptionKeys) + .put(new EntityNumber(2L), new TssEncryptionKeys(NODE2_ENCRYPTION_KEY, Bytes.EMPTY)); + } + + @Test + void ignoresEmptyEncryptionKeys() { + givenConfig(true); + given(ctx.roundNumber()).willReturn(ROUND_NO); + given(ctx.startupNetworks()).willReturn(startupNetworks); + given(startupNetworks.overrideNetworkFor(ROUND_NO)).willReturn(Optional.of(NETWORK_WITHOUT_KEYS)); + given(ctx.newStates()).willReturn(writableStates); + given(writableStates.get(TSS_ENCRYPTION_KEYS_KEY)) + .willReturn(writableEncryptionKeys); + + subject.restart(ctx); + + verify(writableEncryptionKeys, never()).put(any(), any()); + } + + @Test + void setsTssMaterialFromNetworkMessagesIfPresent() { + final var roster = RosterUtils.rosterFrom(NETWORK_WITH_TSS_KEY_MATERIAL); + final var rosterHash = RosterUtils.hash(roster).getBytes(); + + subject.setTssMessageOpsAndVotes(NETWORK_WITH_TSS_KEY_MATERIAL, writableMessages, writableVotes); + + verify(writableMessages) + .put( + new TssMessageMapKey(rosterHash, 0L), + NETWORK_WITH_TSS_KEY_MATERIAL.tssMessages().getFirst()); + verify(writableMessages) + .put( + new TssMessageMapKey(rosterHash, 1L), + NETWORK_WITH_TSS_KEY_MATERIAL.tssMessages().getLast()); + verify(writableVotes).put(eq(new TssVoteMapKey(rosterHash, 1L)), any()); + verify(writableVotes).put(eq(new TssVoteMapKey(rosterHash, 2L)), any()); + } + + private void givenConfig(final boolean tssEnabled) { + final var configBuilder = HederaTestConfigBuilder.create().withValue("tss.keyCandidateRoster", tssEnabled); + given(ctx.appConfig()).willReturn(configBuilder.getOrCreateConfig()); + } +} diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/stores/ReadableTssStoreTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/stores/ReadableTssStoreTest.java index f6040c6f864b..1bd0898c44b1 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/stores/ReadableTssStoreTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/stores/ReadableTssStoreTest.java @@ -18,11 +18,12 @@ 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 com.hedera.node.app.tss.schemas.V0570TssBaseSchema.TSS_ENCRYPTION_KEY_MAP_KEY; -import static com.hedera.node.app.tss.schemas.V0570TssBaseSchema.TSS_STATUS_KEY; +import static com.hedera.node.app.tss.schemas.V0580TssBaseSchema.TSS_ENCRYPTION_KEYS_KEY; import static java.util.Collections.singletonList; import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +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; @@ -35,16 +36,14 @@ import com.hedera.hapi.node.state.common.EntityNumber; import com.hedera.hapi.node.state.roster.Roster; import com.hedera.hapi.node.state.roster.RosterEntry; +import com.hedera.hapi.node.state.tss.TssEncryptionKeys; import com.hedera.hapi.node.state.tss.TssMessageMapKey; -import com.hedera.hapi.node.state.tss.TssStatus; import com.hedera.hapi.node.state.tss.TssVoteMapKey; -import com.hedera.hapi.services.auxiliary.tss.TssEncryptionKeyTransactionBody; import com.hedera.hapi.services.auxiliary.tss.TssMessageTransactionBody; import com.hedera.hapi.services.auxiliary.tss.TssVoteTransactionBody; import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.platform.state.service.ReadableRosterStore; import com.swirlds.state.spi.ReadableKVState; -import com.swirlds.state.spi.ReadableSingletonState; import com.swirlds.state.spi.ReadableStates; import java.util.BitSet; import java.util.List; @@ -68,10 +67,7 @@ class ReadableTssStoreTest { private ReadableKVState readableTssVoteState; @Mock - private ReadableKVState readableTssEncryptionKeyState; - - @Mock - private ReadableSingletonState readableTssStatusState; + private ReadableKVState readableTssEncryptionKeyState; @Mock private ReadableStates states; @@ -109,9 +105,8 @@ void setUp() { .thenReturn(readableTssMessageState); when(states.get(TSS_VOTE_MAP_KEY)) .thenReturn(readableTssVoteState); - when(states.get(TSS_ENCRYPTION_KEY_MAP_KEY)) + when(states.get(TSS_ENCRYPTION_KEYS_KEY)) .thenReturn(readableTssEncryptionKeyState); - when(states.getSingleton(TSS_STATUS_KEY)).thenReturn(readableTssStatusState); tssStore = new ReadableTssStoreImpl(states); } @@ -217,6 +212,18 @@ void testExistsVote() { assertTrue(tssStore.exists(key)); } + @Test + void testGetsAllVotes() { + TssVoteTransactionBody vote = TssVoteTransactionBody.DEFAULT; + when(readableTssVoteState.keys()) + .thenReturn(singletonList(TssVoteMapKey.DEFAULT).iterator()); + when(readableTssVoteState.get(TssVoteMapKey.DEFAULT)).thenReturn(vote); + + final var result = tssStore.allVotes(); + assertEquals(1, result.size()); + assertEquals(vote, result.get(0)); + } + @Test void testGetMessagesForTarget() { Bytes rosterHash = Bytes.wrap("targetHash".getBytes()); @@ -235,20 +242,9 @@ void testGetMessagesForTarget() { void testGetTssEncryptionKey() { long nodeID = 123L; EntityNumber entityNumber = new EntityNumber(nodeID); - TssEncryptionKeyTransactionBody encryptionKey = TssEncryptionKeyTransactionBody.DEFAULT; - when(readableTssEncryptionKeyState.get(entityNumber)).thenReturn(encryptionKey); - - TssEncryptionKeyTransactionBody result = tssStore.getTssEncryptionKey(nodeID); - assertEquals(encryptionKey, result); - } - - @Test - void testGetTssStatus() { - TssStatus status = TssStatus.DEFAULT; - when(readableTssStatusState.get()).thenReturn(status); - - TssStatus result = tssStore.getTssStatus(); - assertEquals(status, result); + when(readableTssEncryptionKeyState.get(entityNumber)).thenReturn(TssEncryptionKeys.DEFAULT); + final var keys = tssStore.getTssEncryptionKeys(nodeID); + assertSame(TssEncryptionKeys.DEFAULT, keys); } @Test @@ -268,4 +264,42 @@ void testAnyWinningVoteFrom() { tssStore.anyWinningVoteFrom(sourceRosterHash, targetRosterHash, 10L, weightFn); assertTrue(result.isPresent()); } + + @Test + void testAnyWinningVoteForWhenNoVote() { + Bytes sourceRosterHash = Bytes.wrap("sourceHash".getBytes()); + Bytes targetRosterHash = Bytes.wrap("targetHash".getBytes()); + TssVoteMapKey key = mock(TssVoteMapKey.class); + TssVoteTransactionBody vote = TssVoteTransactionBody.newBuilder() + .sourceRosterHash(sourceRosterHash) + .tssVote(Bytes.wrap("vote".getBytes())) + .targetRosterHash(targetRosterHash) + .build(); + when(readableTssVoteState.keys()).thenReturn(singletonList(key).iterator()); + when(readableTssVoteState.get(key)).thenReturn(vote); + when(rosterStore.get(sourceRosterHash)).thenReturn(SOURCE_ROSTER); + + Optional result = tssStore.anyWinningVoteFor(targetRosterHash, rosterStore); + assertFalse(result.isPresent()); + } + + @Test + void testAnyWinningVoteFor() { + doCallRealMethod().when(subject).anyWinningVoteFor(any(), any()); + Bytes sourceRosterHash = Bytes.wrap("sourceHash".getBytes()); + Bytes targetRosterHash = Bytes.wrap("targetHash".getBytes()); + TssVoteMapKey key = + TssVoteMapKey.newBuilder().rosterHash(targetRosterHash).build(); + TssVoteTransactionBody vote = TssVoteTransactionBody.newBuilder() + .sourceRosterHash(sourceRosterHash) + .tssVote(Bytes.wrap("vote".getBytes())) + .targetRosterHash(targetRosterHash) + .build(); + when(subject.allVotes()).thenReturn(List.of(vote)); + when(subject.anyWinningVoteFrom(any(), any(), any())).thenReturn(Optional.of(vote)); + + Optional result = subject.anyWinningVoteFor(targetRosterHash, rosterStore); + + assertTrue(result.isPresent()); + } } diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/stores/WritableTssStoreTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/stores/WritableTssStoreTest.java index 399f3bee327a..a71d546212a0 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/stores/WritableTssStoreTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/stores/WritableTssStoreTest.java @@ -18,19 +18,16 @@ 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 com.hedera.node.app.tss.schemas.V0570TssBaseSchema.TSS_ENCRYPTION_KEY_MAP_KEY; -import static com.hedera.node.app.tss.schemas.V0570TssBaseSchema.TSS_STATUS_KEY; +import static com.hedera.node.app.tss.schemas.V0580TssBaseSchema.TSS_ENCRYPTION_KEYS_KEY; import static org.mockito.Mockito.*; import com.hedera.hapi.node.state.common.EntityNumber; import com.hedera.hapi.node.state.tss.TssMessageMapKey; -import com.hedera.hapi.node.state.tss.TssStatus; import com.hedera.hapi.node.state.tss.TssVoteMapKey; import com.hedera.hapi.services.auxiliary.tss.TssEncryptionKeyTransactionBody; 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.WritableSingletonState; import com.swirlds.state.spi.WritableStates; import java.util.Iterator; import org.junit.jupiter.api.BeforeEach; @@ -50,9 +47,6 @@ class WritableTssStoreTest { @Mock private WritableKVState tssEncryptionKeyState; - @Mock - private WritableSingletonState tssStatusState; - @Mock private WritableStates states; @@ -64,9 +58,8 @@ void setUp() { .thenReturn(tssMessageState); when(states.get(TSS_VOTE_MAP_KEY)) .thenReturn(tssVoteState); - when(states.get(TSS_ENCRYPTION_KEY_MAP_KEY)) + when(states.get(TSS_ENCRYPTION_KEYS_KEY)) .thenReturn(tssEncryptionKeyState); - when(states.getSingleton(TSS_STATUS_KEY)).thenReturn(tssStatusState); tssStore = new WritableTssStore(states); } @@ -95,13 +88,6 @@ void testPutEncryptionKey() { verify(tssEncryptionKeyState).put(entityNumber, body); } - @Test - void testPutTssStatus() { - TssStatus status = TssStatus.DEFAULT; - tssStore.put(status); - verify(tssStatusState).put(status); - } - @Test void testRemoveTssMessage() { TssMessageMapKey key = TssMessageMapKey.DEFAULT; @@ -134,6 +120,5 @@ void testClear() { verify(tssVoteState).keys(); verify(tssMessageState).keys(); verify(tssEncryptionKeyState).keys(); - verify(tssStatusState).put(TssStatus.DEFAULT); } } 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 d428f98429a1..ff6e88822e3c 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 @@ -28,6 +28,7 @@ import static com.hedera.node.app.records.schemas.V0490BlockRecordSchema.BLOCK_INFO_STATE_KEY; import static com.hedera.node.app.records.schemas.V0490BlockRecordSchema.RUNNING_HASHES_STATE_KEY; import static com.swirlds.platform.state.service.PlatformStateService.PLATFORM_STATE_SERVICE; +import static com.swirlds.platform.state.service.schemas.V0540PlatformStateSchema.UNINITIALIZED_PLATFORM_STATE; import static com.swirlds.state.lifecycle.HapiUtils.asAccountString; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -38,7 +39,6 @@ import com.hedera.hapi.node.base.Timestamp; import com.hedera.hapi.node.state.blockrecords.BlockInfo; import com.hedera.hapi.node.state.blockrecords.RunningHashes; -import com.hedera.node.app.blocks.impl.BlockStreamManagerImpl; import com.hedera.node.app.fixtures.AppTestBase; import com.hedera.node.app.info.NodeInfoImpl; import com.hedera.node.app.records.BlockRecordService; @@ -51,6 +51,7 @@ 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.schemas.V0490BlockRecordSchema; +import com.hedera.node.app.version.ServicesSoftwareVersion; import com.hedera.node.config.data.BlockRecordStreamConfig; import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.platform.state.service.PlatformStateService; @@ -77,7 +78,6 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; -import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) @@ -104,11 +104,9 @@ final class BlockRecordManagerTest extends AppTestBase { private BlockRecordFormat blockRecordFormat; private BlockRecordWriterFactory blockRecordWriterFactory; - @Mock - private BlockStreamManagerImpl blockStreamManager; - @BeforeEach void setUpEach() throws Exception { + PLATFORM_STATE_SERVICE.setAppVersionFn(ServicesSoftwareVersion::from); // create in memory temp dir fs = Jimfs.newFileSystem(Configuration.unix()); final var tempDir = fs.getPath("/temp"); @@ -140,8 +138,7 @@ RUNNING_HASHES_STATE_KEY, new RunningHashes(STARTING_RUNNING_HASH_OBJ.hash(), nu new BlockInfo(-1, EPOCH, STARTING_RUNNING_HASH_OBJ.hash(), null, false, EPOCH)) .commit(); app.stateMutator(PlatformStateService.NAME) - .withSingletonState( - V0540PlatformStateSchema.PLATFORM_STATE_KEY, V0540PlatformStateSchema.GENESIS_PLATFORM_STATE) + .withSingletonState(V0540PlatformStateSchema.PLATFORM_STATE_KEY, UNINITIALIZED_PLATFORM_STATE) .commit(); blockRecordWriterFactory = new BlockRecordWriterFactoryImpl(app.configProvider(), NODE_INFO, SIGNER, fs); 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 7b603277b197..b9e2c67cc19a 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 @@ -44,6 +44,7 @@ import com.hedera.hapi.node.transaction.ExchangeRateSet; import com.hedera.node.app.fees.ExchangeRateManager; import com.hedera.node.app.records.ReadableBlockRecordStore; +import com.hedera.node.app.roster.schemas.V0540RosterSchema; import com.hedera.node.app.service.addressbook.AddressBookService; import com.hedera.node.app.service.addressbook.ReadableNodeStore; import com.hedera.node.app.service.addressbook.impl.ReadableNodeStoreImpl; @@ -61,7 +62,6 @@ import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.config.api.Configuration; import com.swirlds.platform.roster.RosterUtils; -import com.swirlds.platform.state.service.schemas.V0540RosterSchema; import com.swirlds.state.spi.WritableKVState; import com.swirlds.state.spi.WritableSingletonState; import com.swirlds.state.spi.WritableStates; @@ -164,6 +164,11 @@ void processUpdateCalledForGenesisTxn() { given(context.configuration()).willReturn(DEFAULT_CONFIG); given(stack.getWritableStates(AddressBookService.NAME)).willReturn(writableStates); given(writableStates.get(NODES_KEY)).willReturn(nodesState); + given(blockStore.getLastBlockInfo()) + .willReturn(BlockInfo.newBuilder() + .consTimeOfLastHandledTxn(Timestamp.newBuilder().seconds(1_234_567L)) + .build()); + given(context.consensusTime()).willReturn(CONSENSUS_TIME_1234567); subject.process(dispatch, stack, context, RECORDS, true, Instant.EPOCH); diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/steps/PlatformStateUpdatesTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/steps/PlatformStateUpdatesTest.java index 1adc374da2f7..7d0c026f048f 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/steps/PlatformStateUpdatesTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/steps/PlatformStateUpdatesTest.java @@ -18,10 +18,10 @@ import static com.hedera.hapi.node.freeze.FreezeType.FREEZE_UPGRADE; import static com.hedera.node.app.fixtures.AppTestBase.DEFAULT_CONFIG; +import static com.hedera.node.app.roster.schemas.V0540RosterSchema.ROSTER_KEY; +import static com.hedera.node.app.roster.schemas.V0540RosterSchema.ROSTER_STATES_KEY; import static com.hedera.node.app.service.addressbook.impl.schemas.V053AddressBookSchema.NODES_KEY; import static com.hedera.node.app.service.networkadmin.impl.schemas.V0490FreezeSchema.FREEZE_TIME_KEY; -import static com.swirlds.platform.state.service.schemas.V0540RosterSchema.ROSTER_KEY; -import static com.swirlds.platform.state.service.schemas.V0540RosterSchema.ROSTER_STATES_KEY; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mock.Strictness.LENIENT; @@ -63,8 +63,8 @@ import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) -class PlatformStateUpdatesTest implements TransactionFactory { - private static final RosterState ROSTER_STATE = RosterState.newBuilder() +public class PlatformStateUpdatesTest implements TransactionFactory { + public static final RosterState ROSTER_STATE = RosterState.newBuilder() .roundRosterPairs(List.of(RoundRosterPair.DEFAULT)) .build(); private FakeState state; @@ -82,7 +82,7 @@ class PlatformStateUpdatesTest implements TransactionFactory { @BeforeEach void setUp() { freezeTimeBackingStore = new AtomicReference<>(null); - platformStateBackingStore = new AtomicReference<>(V0540PlatformStateSchema.GENESIS_PLATFORM_STATE); + platformStateBackingStore = new AtomicReference<>(V0540PlatformStateSchema.UNINITIALIZED_PLATFORM_STATE); when(writableStates.getSingleton(FREEZE_TIME_KEY)) .then(invocation -> new WritableSingletonStateBase<>( FREEZE_TIME_KEY, freezeTimeBackingStore::get, freezeTimeBackingStore::set)); 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 fa945f5cd98c..c07adf82a303 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 @@ -22,7 +22,6 @@ import static com.hedera.node.app.util.FileUtilities.createFileID; import static com.hedera.node.app.workflows.standalone.TransactionExecutors.DEFAULT_NODE_INFO; import static com.hedera.node.app.workflows.standalone.TransactionExecutors.TRANSACTION_EXECUTORS; -import static com.swirlds.platform.roster.RosterRetriever.buildRoster; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import com.hedera.hapi.node.base.AccountID; @@ -31,13 +30,13 @@ import com.hedera.hapi.node.base.FileID; import com.hedera.hapi.node.base.HederaFunctionality; import com.hedera.hapi.node.base.KeyList; +import com.hedera.hapi.node.base.ServiceEndpoint; import com.hedera.hapi.node.base.Timestamp; import com.hedera.hapi.node.base.TransactionID; import com.hedera.hapi.node.contract.ContractCallTransactionBody; import com.hedera.hapi.node.contract.ContractCreateTransactionBody; import com.hedera.hapi.node.file.FileCreateTransactionBody; import com.hedera.hapi.node.state.file.File; -import com.hedera.hapi.node.state.roster.Roster; import com.hedera.hapi.node.transaction.ThrottleDefinitions; import com.hedera.hapi.node.transaction.TransactionBody; import com.hedera.node.app.config.BootstrapConfigProviderImpl; @@ -321,6 +320,7 @@ private void registerServices( } private static NetworkInfo fakeNetworkInfo() { + final AccountID someAccount = AccountID.newBuilder().accountNum(12345).build(); final var addressBook = new AddressBook(StreamSupport.stream( Spliterators.spliteratorUnknownSize( RandomAddressBookBuilder.create(new Random()) @@ -343,19 +343,29 @@ public Bytes ledgerId() { @NonNull @Override public NodeInfo selfNodeInfo() { - return new NodeInfoImpl(0, AccountID.DEFAULT, 0, List.of(), getCertBytes(randomX509Certificate())); + return new NodeInfoImpl( + 0, + someAccount, + 0, + List.of(ServiceEndpoint.DEFAULT, ServiceEndpoint.DEFAULT), + getCertBytes(randomX509Certificate())); } @NonNull @Override public List addressBook() { - return List.of( - new NodeInfoImpl(0, AccountID.DEFAULT, 0, List.of(), getCertBytes(randomX509Certificate()))); + return List.of(new NodeInfoImpl( + 0, + someAccount, + 0, + List.of(ServiceEndpoint.DEFAULT, ServiceEndpoint.DEFAULT), + getCertBytes(randomX509Certificate()))); } @Override public NodeInfo nodeInfo(final long nodeId) { - return new NodeInfoImpl(0, AccountID.DEFAULT, 0, List.of(), Bytes.EMPTY); + return new NodeInfoImpl( + 0, someAccount, 0, List.of(ServiceEndpoint.DEFAULT, ServiceEndpoint.DEFAULT), Bytes.EMPTY); } @Override @@ -367,11 +377,6 @@ public boolean containsNode(final long nodeId) { public void updateFrom(final State state) { throw new UnsupportedOperationException("Not implemented"); } - - @Override - public Roster roster() { - return buildRoster(addressBook); - } }; } diff --git a/hedera-node/hedera-app/src/test/resources/bootstrap/network.json b/hedera-node/hedera-app/src/test/resources/bootstrap/network.json index 8a19f6db7e5f..f579261810d8 100644 --- a/hedera-node/hedera-app/src/test/resources/bootstrap/network.json +++ b/hedera-node/hedera-app/src/test/resources/bootstrap/network.json @@ -28,7 +28,8 @@ "adminKey": { "ed25519": "CqjiEGTGHquG4qnBZFZbTnqaQUYQbgps0DqMOVoRDpI=" } - } + }, + "tssEncryptionKey": "ASM=" }, { "rosterEntry": { "nodeId": "1", @@ -60,7 +61,8 @@ "adminKey": { "ed25519": "CqjiEGTGHquG4qnBZFZbTnqaQUYQbgps0DqMOVoRDpI=" } - } + }, + "tssEncryptionKey": "ASM=" }, { "rosterEntry": { "nodeId": "2", @@ -92,7 +94,8 @@ "adminKey": { "ed25519": "CqjiEGTGHquG4qnBZFZbTnqaQUYQbgps0DqMOVoRDpI=" } - } + }, + "tssEncryptionKey": "ASM=" }, { "rosterEntry": { "nodeId": "3", @@ -124,7 +127,8 @@ "adminKey": { "ed25519": "CqjiEGTGHquG4qnBZFZbTnqaQUYQbgps0DqMOVoRDpI=" } - } + }, + "tssEncryptionKey": "ASM=" }], "tssMessages": [{ "targetRosterHash": "x59s9F7BvXzYVRKj6jYJq9qMuzV8pgchc053L920tMaWeGNzXoBXhW1x25snK0by", 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 d456e5430c5e..a88aa73998c0 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 @@ -100,6 +100,9 @@ public class AppTestBase extends TestBase implements TransactionFactory, Scenari public static final ScheduledExecutorService METRIC_EXECUTOR = Executors.newSingleThreadScheduledExecutor(); public static final Configuration DEFAULT_CONFIG = HederaTestConfigBuilder.createConfig(); + public static final Configuration WITH_ROSTER_LIFECYCLE = HederaTestConfigBuilder.create() + .withValue("addressBook.useRosterLifecycle", true) + .getOrCreateConfig(); private static final String ACCOUNTS_KEY = "ACCOUNTS"; private static final String ALIASES_KEY = "ALIASES"; @@ -324,7 +327,7 @@ public App build() { realSelfNodeInfo = new NodeInfoImpl( selfNodeInfo.nodeId(), selfNodeInfo.accountId(), - selfNodeInfo.stake(), + selfNodeInfo.weight(), selfNodeInfo.gossipEndpoints(), selfNodeInfo.sigCertBytes()); } @@ -335,19 +338,20 @@ public App build() { final var addresses = nodes.stream() .map(nodeInfo -> new Address() .copySetNodeId(NodeId.of(nodeInfo.nodeId())) - .copySetWeight(nodeInfo.zeroStake() ? 0 : 10)) + .copySetWeight(nodeInfo.zeroWeight() ? 0 : 10)) .toList(); final var addressBook = new AddressBook(addresses); final var platform = new FakePlatform(realSelfNodeInfo.nodeId(), addressBook); final var initialState = new FakeState(); final var genesisRoster = buildRoster(addressBook); - final var networkInfo = new GenesisNetworkInfo(genesisRoster, Bytes.fromHex("03")); - final var startupNetworks = new FakeStartupNetworks(Network.newBuilder() + final var genesisNetwork = Network.newBuilder() .nodeMetadata(genesisRoster.rosterEntries().stream() .map(entry -> NodeMetadata.newBuilder().rosterEntry(entry).build()) .toList()) - .build()); + .build(); + final var networkInfo = new GenesisNetworkInfo(genesisNetwork, Bytes.fromHex("03")); + final var startupNetworks = new FakeStartupNetworks(genesisNetwork); services.forEach(svc -> { final var reg = new FakeSchemaRegistry(); svc.registerSchemas(reg); 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 3c9907532773..51e122ffc25f 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 @@ -79,6 +79,7 @@ public void migrate( CURRENT_VERSION, networkInfo, DEFAULT_CONFIG, + DEFAULT_CONFIG, new HashMap<>(), new AtomicLong(), startupNetworks); @@ -89,10 +90,19 @@ public void migrate( @NonNull final FakeState state, @Nullable final SemanticVersion previousVersion, @NonNull final NetworkInfo networkInfo, - @NonNull final Configuration config, + @NonNull final Configuration appConfig, + @NonNull final Configuration platformConfig, @NonNull final Map sharedValues, @NonNull final AtomicLong nextEntityNum, @NonNull final StartupNetworks startupNetworks) { + requireNonNull(serviceName); + requireNonNull(state); + requireNonNull(networkInfo); + requireNonNull(appConfig); + requireNonNull(platformConfig); + requireNonNull(sharedValues); + requireNonNull(nextEntityNum); + requireNonNull(startupNetworks); if (schemas.isEmpty()) { logger.info("Service {} does not use state", serviceName); return; @@ -108,14 +118,14 @@ public void migrate( () -> HapiUtils.toString(latestVersion)); for (final var schema : schemas) { final var applications = - schemaApplications.computeApplications(previousVersion, latestVersion, schema, config); + schemaApplications.computeApplications(previousVersion, latestVersion, schema, appConfig); logger.info("Applying {} schema {} ({})", serviceName, schema.getVersion(), applications); final var readableStates = state.getReadableStates(serviceName); final var previousStates = new FilteredReadableStates(readableStates, readableStates.stateKeys()); final WritableStates writableStates; final WritableStates newStates; if (applications.contains(STATE_DEFINITIONS)) { - final var redefinedWritableStates = applyStateDefinitions(serviceName, schema, config, state); + final var redefinedWritableStates = applyStateDefinitions(serviceName, schema, appConfig, state); writableStates = redefinedWritableStates.beforeStates(); newStates = redefinedWritableStates.afterStates(); } else { @@ -125,7 +135,8 @@ public void migrate( previousVersion, previousStates, newStates, - config, + appConfig, + platformConfig, networkInfo, nextEntityNum, sharedValues, @@ -183,7 +194,8 @@ private MigrationContext newMigrationContext( @Nullable final SemanticVersion previousVersion, @NonNull final ReadableStates previousStates, @NonNull final WritableStates writableStates, - @NonNull final Configuration config, + @NonNull final Configuration appConfig, + @NonNull final Configuration platformConfig, @NonNull final NetworkInfo networkInfo, @NonNull final AtomicLong nextEntityNum, @NonNull final Map sharedValues, @@ -224,8 +236,14 @@ public WritableStates newStates() { @NonNull @Override - public Configuration configuration() { - return config; + public Configuration appConfig() { + return appConfig; + } + + @NonNull + @Override + public Configuration platformConfig() { + return platformConfig; } @Override diff --git a/hedera-node/hedera-app/src/testFixtures/java/com/hedera/node/app/fixtures/state/FakeServiceMigrator.java b/hedera-node/hedera-app/src/testFixtures/java/com/hedera/node/app/fixtures/state/FakeServiceMigrator.java index 37b183151a17..1d8c59743bb2 100644 --- a/hedera-node/hedera-app/src/testFixtures/java/com/hedera/node/app/fixtures/state/FakeServiceMigrator.java +++ b/hedera-node/hedera-app/src/testFixtures/java/com/hedera/node/app/fixtures/state/FakeServiceMigrator.java @@ -50,16 +50,16 @@ public List doMigrations( @NonNull final ServicesRegistry servicesRegistry, @Nullable final SoftwareVersion previousVersion, @NonNull final SoftwareVersion currentVersion, - @NonNull final Configuration nodeConfiguration, - @NonNull final Configuration platformConfiguration, + @NonNull final Configuration appConfig, + @NonNull final Configuration platformConfig, @Nullable final NetworkInfo genesisNetworkInfo, @NonNull final Metrics metrics, @NonNull final StartupNetworks startupNetworks) { requireNonNull(state); requireNonNull(servicesRegistry); requireNonNull(currentVersion); - requireNonNull(nodeConfiguration); - requireNonNull(platformConfiguration); + requireNonNull(appConfig); + requireNonNull(platformConfig); requireNonNull(genesisNetworkInfo); requireNonNull(metrics); @@ -70,8 +70,8 @@ public List doMigrations( throw new IllegalArgumentException("Can only be used with FakeServicesRegistry instances"); } - final AtomicLong prevEntityNum = new AtomicLong( - nodeConfiguration.getConfigData(HederaConfig.class).firstUserEntity() - 1); + final AtomicLong prevEntityNum = + new AtomicLong(appConfig.getConfigData(HederaConfig.class).firstUserEntity() - 1); final Map sharedValues = new HashMap<>(); final var entityIdRegistration = registry.registrations().stream() .filter(service -> @@ -89,7 +89,8 @@ public List doMigrations( fakeState, deserializedPbjVersion, genesisNetworkInfo, - nodeConfiguration, + appConfig, + platformConfig, sharedValues, prevEntityNum, startupNetworks); @@ -104,7 +105,8 @@ public List doMigrations( fakeState, deserializedPbjVersion, genesisNetworkInfo, - platformConfiguration, + appConfig, + platformConfig, sharedValues, prevEntityNum, startupNetworks); 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 00f8e0bb9613..63ca23f43e83 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 @@ -38,7 +38,7 @@ */ @ConfigData("tss") public record TssConfig( - @ConfigProperty(defaultValue = "3") @NetworkProperty long maxSharesPerNode, + @ConfigProperty(defaultValue = "3") @NetworkProperty int maxSharesPerNode, @ConfigProperty(defaultValue = "50") @NetworkProperty int timesToTrySubmission, @ConfigProperty(defaultValue = "5s") @NetworkProperty Duration retryDelay, @ConfigProperty(defaultValue = "10") @NetworkProperty int distinctTxnIdsToTry, diff --git a/hedera-node/hedera-file-service-impl/src/main/java/com/hedera/node/app/service/file/impl/schemas/V0490FileSchema.java b/hedera-node/hedera-file-service-impl/src/main/java/com/hedera/node/app/service/file/impl/schemas/V0490FileSchema.java index 978a63c5ff88..40d0efb212b9 100644 --- a/hedera-node/hedera-file-service-impl/src/main/java/com/hedera/node/app/service/file/impl/schemas/V0490FileSchema.java +++ b/hedera-node/hedera-file-service-impl/src/main/java/com/hedera/node/app/service/file/impl/schemas/V0490FileSchema.java @@ -217,7 +217,7 @@ public Bytes genesisNodeDetails(@NonNull final NetworkInfo networkInfo) { final var nodeDetails = new ArrayList(); for (final var nodeInfo : networkInfo.addressBook()) { nodeDetails.add(NodeAddress.newBuilder() - .stake(nodeInfo.stake()) + .stake(nodeInfo.weight()) .nodeAccountId(nodeInfo.accountId()) .nodeId(nodeInfo.nodeId()) .rsaPubKey(nodeInfo.hexEncodedPublicKey()) 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 340ca8a4a885..0a00faf9b53c 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 @@ -230,7 +230,7 @@ static TransactionID transactionIdForScheduled(@NonNull final Schedule schedule) * @param schedulingTxnId the scheduling transaction ID * @return the scheduled transaction ID */ - static TransactionID scheduledTxnIdFrom(@NonNull final TransactionID schedulingTxnId) { + public static TransactionID scheduledTxnIdFrom(@NonNull final TransactionID schedulingTxnId) { requireNonNull(schedulingTxnId); return schedulingTxnId.scheduled() ? schedulingTxnId 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 6fe624637125..d369e14341d8 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 @@ -90,7 +90,7 @@ public EndOfStakingPeriodUpdater( * Updates all (relevant) staking-related values for all nodes, as well as any network reward information, * at the end of a staking period. This method must be invoked during handling of a transaction * - * @param context the context of the transaction used to end the staking period + * @param context the context of the transaction used to end the staking period * @param exchangeRates the active exchange rate set * @param weightUpdates the callback to use to propagate weight changes */ @@ -209,9 +209,11 @@ public EndOfStakingPeriodUpdater( // We rescale the weight range [0, sumOfConsensusWeights] back to [minStake, maxStake] before // externalizing the node stake metadata to stream consumers like mirror nodes final var rescaledWeight = rescaleWeight(newWeight, nodeInfo.minStake(), maxStake, totalStake, totalWeight); - nodeStakes.add(EndOfStakingPeriodUtils.fromStakingInfo( - nodeRewardRates.get(nodeId), - nodeInfo.copyBuilder().stake(rescaledWeight).build())); + if (!nodeInfo.deleted()) { + nodeStakes.add(EndOfStakingPeriodUtils.fromStakingInfo( + nodeRewardRates.get(nodeId), + nodeInfo.copyBuilder().stake(rescaledWeight).build())); + } // Persist the updated staking info stakingInfoStore.put(nodeId, newNodeInfo); }); @@ -253,10 +255,10 @@ public EndOfStakingPeriodUpdater( * Scales up the weight of the node to the range [minStake, maxStakeOfAllNodes] * from the consensus weight range [0, sumOfConsensusWeights]. * - * @param weight weight of the node - * @param newMinStake min stake of the node - * @param newMaxStake real max stake of all nodes computed by taking max(stakeOfNode1, stakeOfNode2, ...) - * @param totalStakeOfAllNodes total stake of all nodes at the start of new period + * @param weight weight of the node + * @param newMinStake min stake of the node + * @param newMaxStake real max stake of all nodes computed by taking max(stakeOfNode1, stakeOfNode2, ...) + * @param totalStakeOfAllNodes total stake of all nodes at the start of new period * @param sumOfConsensusWeights sum of consensus weights of all nodes * @return scaled weight of the node */ @@ -308,8 +310,9 @@ public static long rescaleWeight( * The result are normalized weights whose sum will be approximately the given total weight. That is, any node * with a non-zero amount of stake will have a weight of at least {@code 1}; any node with a stake of at least one * out of every 250 whole hbars staked will have weight at least {@code 2}; and so on. - * @param nodeStake the stake of a single node, both rewarded and non-rewarded - * @param totalStake the total stake of all nodes + * + * @param nodeStake the stake of a single node, both rewarded and non-rewarded + * @param totalStake the total stake of all nodes * @param totalWeight the desired approximate total weight of all nodes * @return the scaled consensus weight for the node */ @@ -356,7 +359,7 @@ private long rewardRateGiven( * threshold, from 0 for empty, up to 1 at the threshold. * * @param unreservedBalance the balance in {@code 0.0.800} minus the pending rewards - * @param thresholdBalance the threshold balance setting + * @param thresholdBalance the threshold balance setting * @return the ratio of the balance to the threshold, from 0 for empty, up to 1 at the threshold */ private BigDecimal ratioOf(final long unreservedBalance, final long thresholdBalance) { @@ -371,9 +374,9 @@ private BigDecimal ratioOf(final long unreservedBalance, final long thresholdBal * start of the period that is now ending, and the maximum amount of tinybars to pay as staking rewards in the * period, returns the effective per-hbar reward rate for the period. * - * @param balanceRatio the ratio of the {@code 0.0.800} balance to the threshold - * @param stakedToReward the amount of hbars staked to reward at the start of the ending period - * @param maxRewardRate the maximum amount of tinybars to pay per hbar reward + * @param balanceRatio the ratio of the {@code 0.0.800} balance to the threshold + * @param stakedToReward the amount of hbars staked to reward at the start of the ending period + * @param maxRewardRate the maximum amount of tinybars to pay per hbar reward * @param maxStakeRewarded the maximum amount of stake that can be rewarded * @return the effective per-hbar reward rate for the period */ 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 c48dbd412546..e4380997c5fa 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 @@ -231,9 +231,11 @@ public StreamBuilder adjustPostUpgradeStakes( final var nodeStakes = new ArrayList(); postUpgradeNodeIds.stream().sorted().forEach(nodeId -> { final var stakingInfo = requireNonNull(infoStore.getForModify(nodeId)); - final var history = stakingInfo.rewardSumHistory(); - final var rewardRate = stakingInfo.deleted() ? 0 : history.getFirst() - history.get(1); - nodeStakes.add(fromStakingInfo(rewardRate, stakingInfo)); + if (!stakingInfo.deleted()) { + final var history = stakingInfo.rewardSumHistory(); + final var rewardRate = history.getFirst() - history.get(1); + nodeStakes.add(fromStakingInfo(rewardRate, stakingInfo)); + } }); final var stakingConfig = config.getConfigData(StakingConfig.class); final var syntheticNodeStakeUpdateTxn = newNodeStakeUpdateBuilder( diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/schemas/V0490TokenSchema.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/schemas/V0490TokenSchema.java index c48f5a3cc208..5d8c2fafa15d 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/schemas/V0490TokenSchema.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/schemas/V0490TokenSchema.java @@ -129,13 +129,13 @@ private void createGenesisSchema(@NonNull final MigrationContext ctx) { } // We will use these various configs for creating accounts. It would be nice to consolidate them somehow - final var ledgerConfig = ctx.configuration().getConfigData(LedgerConfig.class); - final var hederaConfig = ctx.configuration().getConfigData(HederaConfig.class); - final var accountsConfig = ctx.configuration().getConfigData(AccountsConfig.class); + final var ledgerConfig = ctx.appConfig().getConfigData(LedgerConfig.class); + final var hederaConfig = ctx.appConfig().getConfigData(HederaConfig.class); + final var accountsConfig = ctx.appConfig().getConfigData(AccountsConfig.class); // Generate synthetic accounts based on the genesis configuration final Consumer> noOpCb = ignore -> {}; - syntheticAccountCreator.generateSyntheticAccounts(ctx.configuration(), noOpCb, noOpCb, noOpCb, noOpCb, noOpCb); + syntheticAccountCreator.generateSyntheticAccounts(ctx.appConfig(), noOpCb, noOpCb, noOpCb, noOpCb, noOpCb); // ---------- Create system accounts ------------------------- for (final Account acct : syntheticAccountCreator.systemAccounts()) { accounts.put(acct.accountIdOrThrow(), acct); @@ -237,7 +237,7 @@ public static long[] nonContractSystemNums(final long numReservedSystemEntities) } private void initializeStakingNodeInfo(@NonNull final MigrationContext ctx) { - final var config = ctx.configuration(); + final var config = ctx.appConfig(); final var ledgerConfig = config.getConfigData(LedgerConfig.class); final var stakingConfig = config.getConfigData(StakingConfig.class); final var addressBook = ctx.genesisNetworkInfo().addressBook(); diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/schemas/V0530TokenSchema.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/schemas/V0530TokenSchema.java index 0e1face5b312..b947535d63df 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/schemas/V0530TokenSchema.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/schemas/V0530TokenSchema.java @@ -17,6 +17,7 @@ package com.hedera.node.app.service.token.impl.schemas; import static com.hedera.node.app.service.token.impl.schemas.V0490TokenSchema.STAKING_INFO_KEY; +import static java.util.Objects.requireNonNull; import com.hedera.hapi.node.base.PendingAirdropId; import com.hedera.hapi.node.base.SemanticVersion; @@ -26,8 +27,13 @@ import com.swirlds.state.lifecycle.MigrationContext; import com.swirlds.state.lifecycle.Schema; import com.swirlds.state.lifecycle.StateDefinition; +import com.swirlds.state.spi.WritableKVState; import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Comparator; import java.util.Set; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.StreamSupport; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -57,17 +63,16 @@ public void migrate(@NonNull final MigrationContext ctx) { } private void setMinStakeToZero(final MigrationContext ctx) { - final var stakingInfoState = ctx.newStates().get(STAKING_INFO_KEY); - final var addressBook = ctx.genesisNetworkInfo().addressBook(); + final WritableKVState stakingInfos = + ctx.newStates().get(STAKING_INFO_KEY); logger.info("Setting minStake to 0 for all nodes in the address book"); - for (final var node : addressBook) { - final var nodeNumber = - EntityNumber.newBuilder().number(node.nodeId()).build(); - final var stakingInfo = (StakingNodeInfo) stakingInfoState.get(nodeNumber); - if (stakingInfo != null) { - stakingInfoState.put( - nodeNumber, stakingInfo.copyBuilder().minStake(0).build()); - } + final var nodeIds = StreamSupport.stream( + Spliterators.spliteratorUnknownSize(stakingInfos.keys(), Spliterator.NONNULL), false) + .sorted(Comparator.comparingLong(EntityNumber::number)) + .toList(); + for (final var nodeId : nodeIds) { + final var stakingInfo = requireNonNull(stakingInfos.get(nodeId)); + stakingInfos.put(nodeId, stakingInfo.copyBuilder().minStake(0).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 924e5da68ce1..f9a66722d8d0 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 @@ -27,14 +27,18 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import com.hedera.hapi.node.base.Timestamp; +import com.hedera.hapi.node.base.Transaction; import com.hedera.hapi.node.state.common.EntityNumber; import com.hedera.hapi.node.state.token.Account; import com.hedera.hapi.node.state.token.NetworkStakingRewards; import com.hedera.hapi.node.state.token.StakingNodeInfo; import com.hedera.hapi.node.transaction.ExchangeRateSet; +import com.hedera.hapi.node.transaction.SignedTransaction; +import com.hedera.hapi.node.transaction.TransactionBody; import com.hedera.node.app.service.token.ReadableAccountStore; import com.hedera.node.app.service.token.impl.WritableNetworkStakingRewardsStore; import com.hedera.node.app.service.token.impl.WritableStakingInfoStore; @@ -51,6 +55,7 @@ import com.hedera.node.config.ConfigProvider; import com.hedera.node.config.data.StakingConfig; import com.hedera.node.config.testfixtures.HederaTestConfigBuilder; +import com.hedera.pbj.runtime.ParseException; import com.swirlds.config.extensions.test.fixtures.TestConfigBuilder; import com.swirlds.state.spi.WritableSingletonState; import com.swirlds.state.spi.WritableSingletonStateBase; @@ -67,6 +72,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -201,7 +207,8 @@ void scalesBackWeightToStake() { } @Test - void deletedNodesGetsZeroPendingRewards() { + void deletedNodesGetsZeroPendingRewards() throws ParseException { + final var captor = ArgumentCaptor.forClass(Transaction.class); commonSetup( 1_000_000_000L, STAKING_INFO_1.copyBuilder().deleted(true).build(), @@ -218,6 +225,7 @@ void deletedNodesGetsZeroPendingRewards() { given(nodeStakeUpdateRecordBuilder.memo(any())).willReturn(nodeStakeUpdateRecordBuilder); given(nodeStakeUpdateRecordBuilder.exchangeRate(ExchangeRateSet.DEFAULT)) .willReturn(nodeStakeUpdateRecordBuilder); + given(nodeStakeUpdateRecordBuilder.transaction(captor.capture())).willReturn(nodeStakeUpdateRecordBuilder); subject.updateNodes(context, ExchangeRateSet.DEFAULT, weightUpdates); @@ -248,6 +256,18 @@ void deletedNodesGetsZeroPendingRewards() { assertThat(logCaptor.infoLogs()).contains("Non-zero reward sum history for node number 1 is now [6, 6, 5]"); assertThat(logCaptor.infoLogs()).contains("Non-zero reward sum history for node number 2 is now [101, 1, 1]"); assertThat(logCaptor.infoLogs()).contains("Non-zero reward sum history for node number 3 is now [3, 3, 1]"); + + // Doesn't export deleted nodes nodeStakeUpdates + verify(context).addPrecedingChildRecordBuilder(NodeStakeUpdateStreamBuilder.class, NODE_STAKE_UPDATE); + final var transaction = captor.getValue(); + final var nodeStakeUpdate = TransactionBody.PROTOBUF + .parse(SignedTransaction.PROTOBUF + .parse(transaction.signedTransactionBytes()) + .bodyBytes()) + .nodeStakeUpdate(); + final var nodeStakes = nodeStakeUpdate.nodeStake(); + assertThat(nodeStakes).hasSize(1); + assertThat(nodeStakes.get(0).nodeId()).isEqualTo(NODE_NUM_2.number()); } @Test 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 2740101fb88f..9fa8e815cb4d 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 @@ -31,9 +31,13 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; +import com.hedera.hapi.node.base.Transaction; import com.hedera.hapi.node.state.common.EntityNumber; import com.hedera.hapi.node.state.token.StakingNodeInfo; +import com.hedera.hapi.node.transaction.SignedTransaction; +import com.hedera.hapi.node.transaction.TransactionBody; import com.hedera.node.app.service.token.impl.WritableNetworkStakingRewardsStore; import com.hedera.node.app.service.token.impl.WritableStakingInfoStore; import com.hedera.node.app.service.token.impl.handlers.staking.StakeInfoHelper; @@ -42,6 +46,7 @@ import com.hedera.node.app.service.token.records.TokenContext; import com.hedera.node.app.spi.fixtures.info.FakeNetworkInfo; import com.hedera.node.config.testfixtures.HederaTestConfigBuilder; +import com.hedera.pbj.runtime.ParseException; import com.swirlds.config.api.Configuration; import com.swirlds.state.spi.WritableSingletonStateBase; import com.swirlds.state.test.fixtures.MapWritableKVState; @@ -53,6 +58,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -104,7 +110,8 @@ void increaseUnclaimedStartToLargerThanCurrentStakeReward(int amount, int expect } @Test - void marksNonExistingNodesToDeletedInStateAndAddsNewNodesToState() { + void marksNonExistingNodesToDeletedInStateAndAddsNewNodesToState() throws ParseException { + final var captor = ArgumentCaptor.forClass(Transaction.class); // State has nodeIds 1, 2, 3 final var stakingInfosState = new MapWritableKVState.Builder(STAKING_INFO_KEY) .value(NODE_NUM_1, STAKING_INFO_1) @@ -118,7 +125,7 @@ void marksNonExistingNodesToDeletedInStateAndAddsNewNodesToState() { given(tokenContext.consensusTime()).willReturn(Instant.EPOCH); given(tokenContext.addPrecedingChildRecordBuilder(NodeStakeUpdateStreamBuilder.class, NODE_STAKE_UPDATE)) .willReturn(streamBuilder); - given(streamBuilder.transaction(any())).willReturn(streamBuilder); + given(streamBuilder.transaction(captor.capture())).willReturn(streamBuilder); given(streamBuilder.memo(any())).willReturn(streamBuilder); // Should update the state to mark node 1 and 3 as deleted @@ -138,6 +145,20 @@ void marksNonExistingNodesToDeletedInStateAndAddsNewNodesToState() { assertThat(((StakingNodeInfo) updatedStates.get(NODE_NUM_8)).weight()).isZero(); assertThat(((StakingNodeInfo) updatedStates.get(NODE_NUM_8)).minStake()).isZero(); assertThat(((StakingNodeInfo) updatedStates.get(NODE_NUM_8)).maxStake()).isEqualTo(1666666666666666666L); + + // Doesn't export deleted nodes nodeStakeUpdates + verify(tokenContext).addPrecedingChildRecordBuilder(NodeStakeUpdateStreamBuilder.class, NODE_STAKE_UPDATE); + final var transaction = captor.getValue(); + final var nodeStakeUpdate = TransactionBody.PROTOBUF + .parse(SignedTransaction.PROTOBUF + .parse(transaction.signedTransactionBytes()) + .bodyBytes()) + .nodeStakeUpdate(); + final var nodeStakes = nodeStakeUpdate.nodeStake(); + assertThat(nodeStakes).hasSize(3); + assertThat(nodeStakes.get(0).nodeId()).isEqualTo(NODE_NUM_2.number()); + assertThat(nodeStakes.get(1).nodeId()).isEqualTo(NODE_NUM_4.number()); + assertThat(nodeStakes.get(2).nodeId()).isEqualTo(NODE_NUM_8.number()); } private MapWritableStates newStatesInstance(final MapWritableKVState stakingInfo) { diff --git a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/schemas/V0490TokenSchemaTest.java b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/schemas/V0490TokenSchemaTest.java index 2e793c4720f8..8531f229aeb3 100644 --- a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/schemas/V0490TokenSchemaTest.java +++ b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/schemas/V0490TokenSchemaTest.java @@ -118,6 +118,7 @@ void nonGenesisDoesntCreate() { nonEmptyPrevStates, newStates, config, + config, networkInfo, entityIdStore, CURRENT_VERSION, @@ -142,6 +143,7 @@ void initializesStakingDataOnGenesisStart() { EmptyReadableStates.INSTANCE, newStates, config, + config, networkInfo, entityIdStore, null, @@ -164,6 +166,7 @@ void createsAllAccountsOnGenesisStart() { EmptyReadableStates.INSTANCE, newStates, config, + config, networkInfo, entityIdStore, null, @@ -239,6 +242,7 @@ void blocklistNotEnabled() { EmptyReadableStates.INSTANCE, newStates, config, + config, networkInfo, entityIdStore, CURRENT_VERSION, @@ -266,6 +270,7 @@ void onlyExpectedIdsUsed() { EmptyReadableStates.INSTANCE, newStates, config, + config, networkInfo, entityIdStore, null, diff --git a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/schemas/V0530TokenSchemaTest.java b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/schemas/V0530TokenSchemaTest.java index 627e0310bd3d..fad1e6fa8751 100644 --- a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/schemas/V0530TokenSchemaTest.java +++ b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/schemas/V0530TokenSchemaTest.java @@ -109,6 +109,7 @@ void setsStakingInfoMinStakeToZero() { previousStates, newStates, config, + config, networkInfo, entityIdStore, null, diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/BootstrapOverride.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/ConfigOverride.java similarity index 89% rename from hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/BootstrapOverride.java rename to hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/ConfigOverride.java index 8ccae865a57b..c7e4833b00cd 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/BootstrapOverride.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/ConfigOverride.java @@ -17,9 +17,9 @@ package com.hedera.services.bdd.junit; /** - * An override for a bootstrap property. + * A network configuration override. */ -public @interface BootstrapOverride { +public @interface ConfigOverride { String key(); String value(); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/GenesisHapiTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/GenesisHapiTest.java index 50a5b2a941ab..2f9bccfcaff7 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/GenesisHapiTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/GenesisHapiTest.java @@ -42,5 +42,5 @@ @ExtendWith({NetworkTargetingExtension.class, SpecNamingExtension.class}) @Isolated public @interface GenesisHapiTest { - BootstrapOverride[] bootstrapOverrides() default {}; + ConfigOverride[] bootstrapOverrides() default {}; } 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 249f6fbb65fd..aab29477152c 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 @@ -25,6 +25,7 @@ import com.hedera.services.bdd.junit.LeakyHapiTest; import com.hedera.services.bdd.junit.LeakyRepeatableHapiTest; import com.hedera.services.bdd.junit.RepeatableHapiTest; +import com.hedera.services.bdd.junit.restart.RestartHapiTest; import edu.umd.cs.findbugs.annotations.NonNull; import java.lang.reflect.Method; import java.util.Optional; @@ -39,6 +40,7 @@ private static boolean isHapiTest(@NonNull final Method method) { return isAnnotated(method, HapiTest.class) || isAnnotated(method, LeakyHapiTest.class) || isAnnotated(method, GenesisHapiTest.class) + || isAnnotated(method, RestartHapiTest.class) || isAnnotated(method, EmbeddedHapiTest.class) || isAnnotated(method, RepeatableHapiTest.class) || isAnnotated(method, LeakyEmbeddedHapiTest.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 9c421d697751..47fa7e9cf465 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 @@ -20,11 +20,24 @@ import static com.hedera.services.bdd.junit.ContextRequirement.THROTTLE_OVERRIDES; import static com.hedera.services.bdd.junit.extensions.ExtensionUtils.hapiTestMethodOf; import static com.hedera.services.bdd.junit.hedera.embedded.EmbeddedMode.CONCURRENT; +import static com.hedera.services.bdd.junit.hedera.embedded.EmbeddedMode.REPEATABLE; +import static com.hedera.services.bdd.junit.hedera.utils.AddressBookUtils.CLASSIC_ENCRYPTION_KEYS; +import static com.hedera.services.bdd.junit.hedera.utils.AddressBookUtils.CLASSIC_KEY_MATERIAL_GENERATOR; +import static com.hedera.services.bdd.junit.hedera.utils.WorkingDirUtils.workingDirVersion; +import static com.hedera.services.bdd.junit.restart.StartupAssets.ROSTER_AND_ENCRYPTION_KEYS; +import static com.hedera.services.bdd.junit.restart.StartupAssets.ROSTER_AND_FULL_TSS_KEY_MATERIAL; +import static com.hedera.services.bdd.spec.HapiSpec.doTargetSpec; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; import static java.util.Objects.requireNonNull; import static java.util.stream.Collectors.toMap; import static org.junit.platform.commons.support.AnnotationSupport.isAnnotated; -import com.hedera.services.bdd.junit.BootstrapOverride; +import com.hedera.hapi.node.state.roster.Roster; +import com.hedera.hapi.node.state.roster.RosterEntry; +import com.hedera.hapi.util.HapiUtils; +import com.hedera.node.app.fixtures.state.FakeState; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.hedera.services.bdd.junit.ConfigOverride; import com.hedera.services.bdd.junit.ContextRequirement; import com.hedera.services.bdd.junit.GenesisHapiTest; import com.hedera.services.bdd.junit.HapiTest; @@ -34,16 +47,27 @@ import com.hedera.services.bdd.junit.SharedNetworkLauncherSessionListener; import com.hedera.services.bdd.junit.TargetEmbeddedMode; import com.hedera.services.bdd.junit.hedera.HederaNetwork; +import com.hedera.services.bdd.junit.hedera.TssKeyMaterial; import com.hedera.services.bdd.junit.hedera.embedded.EmbeddedMode; import com.hedera.services.bdd.junit.hedera.embedded.EmbeddedNetwork; +import com.hedera.services.bdd.junit.restart.RestartHapiTest; +import com.hedera.services.bdd.junit.restart.SavedStateSpec; +import com.hedera.services.bdd.junit.restart.StartupAssets; import com.hedera.services.bdd.spec.HapiSpec; +import com.hedera.services.bdd.spec.SpecOperation; import com.hedera.services.bdd.spec.keys.RepeatableKeyGenerator; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.util.Arrays; +import java.util.EnumSet; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import java.util.function.LongFunction; import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; @@ -57,9 +81,34 @@ * networks for annotated test classes and targeting them instead of the shared network. */ public class NetworkTargetingExtension implements BeforeEachCallback, AfterEachCallback { + private static final String SPEC_NAME = ""; + private static final Set OVERRIDES_WITH_ENCRYPTION_KEYS = + EnumSet.of(ROSTER_AND_ENCRYPTION_KEYS, ROSTER_AND_FULL_TSS_KEY_MATERIAL); + public static final AtomicReference SHARED_NETWORK = new AtomicReference<>(); public static final AtomicReference REPEATABLE_KEY_GENERATOR = new AtomicReference<>(); + /** + * The functions that provide the TSS encryption key and key material for a TSS node. + * @param tssEncryptionKeyFn the function that provides the TSS encryption key + * @param tssKeyMaterialFn the function that provides the TSS key material + */ + private record TssSourceFns( + @NonNull LongFunction tssEncryptionKeyFn, + @NonNull Function, Optional> tssKeyMaterialFn) { + public static TssSourceFns from(@NonNull final StartupAssets assets) { + requireNonNull(assets); + return new TssSourceFns( + OVERRIDES_WITH_ENCRYPTION_KEYS.contains(assets) + ? CLASSIC_ENCRYPTION_KEYS::get + : nodeId -> Bytes.EMPTY, + assets == ROSTER_AND_FULL_TSS_KEY_MATERIAL + ? rosterEntries -> + Optional.of(CLASSIC_KEY_MATERIAL_GENERATOR.apply(new Roster(rosterEntries))) + : rosterEntries -> Optional.empty()); + } + } + @Override public void beforeEach(@NonNull final ExtensionContext extensionContext) { hapiTestMethodOf(extensionContext).ifPresent(method -> { @@ -68,8 +117,42 @@ public void beforeEach(@NonNull final ExtensionContext extensionContext) { new EmbeddedNetwork(method.getName().toUpperCase(), method.getName(), CONCURRENT); final var a = method.getAnnotation(GenesisHapiTest.class); final var bootstrapOverrides = Arrays.stream(a.bootstrapOverrides()) - .collect(toMap(BootstrapOverride::key, BootstrapOverride::value)); - targetNetwork.startWithOverrides(bootstrapOverrides); + .collect(toMap(ConfigOverride::key, ConfigOverride::value)); + targetNetwork.startWith(bootstrapOverrides, nodeId -> Bytes.EMPTY, nodes -> Optional.empty()); + HapiSpec.TARGET_NETWORK.set(targetNetwork); + } else if (isAnnotated(method, RestartHapiTest.class)) { + final var targetNetwork = + new EmbeddedNetwork(method.getName().toUpperCase(), method.getName(), REPEATABLE); + final var a = method.getAnnotation(RestartHapiTest.class); + + final var setupOverrides = + Arrays.stream(a.setupOverrides()).collect(toMap(ConfigOverride::key, ConfigOverride::value)); + final var setupTssSourceFns = TssSourceFns.from(a.setupAssets()); + + final var restartOverrides = + Arrays.stream(a.restartOverrides()).collect(toMap(ConfigOverride::key, ConfigOverride::value)); + final var restartTssSourceFns = TssSourceFns.from(a.restartAssets()); + + switch (a.restartType()) { + case GENESIS -> targetNetwork.startWith( + restartOverrides, + restartTssSourceFns.tssEncryptionKeyFn(), + restartTssSourceFns.tssKeyMaterialFn()); + case SAME_VERSION -> targetNetwork.startWith( + setupOverrides, + setupTssSourceFns.tssEncryptionKeyFn(), + setupTssSourceFns.tssKeyMaterialFn()); + case UPGRADE_BOUNDARY -> startFromPreviousVersion(targetNetwork, setupOverrides, setupTssSourceFns); + } + switch (a.restartType()) { + case GENESIS -> { + // The restart was from genesis, so nothing else to do + } + case SAME_VERSION, UPGRADE_BOUNDARY -> { + final var state = postGenesisStateOf(targetNetwork, a); + targetNetwork.restart(state, restartOverrides); + } + } HapiSpec.TARGET_NETWORK.set(targetNetwork); } else { ensureEmbeddedNetwork(extensionContext); @@ -100,6 +183,7 @@ public void afterEach(@NonNull final ExtensionContext extensionContext) { /** * Ensures that the embedded network is running, if required by the test class or method. + * * @param extensionContext the extension context */ public static void ensureEmbeddedNetwork(@NonNull final ExtensionContext extensionContext) { @@ -110,6 +194,7 @@ public static void ensureEmbeddedNetwork(@NonNull final ExtensionContext extensi /** * Returns the embedded mode required by the test class or method, if any. + * * @param extensionContext the extension context * @return the embedded mode */ @@ -135,6 +220,7 @@ private void bindThreadTargets( /** * If there is an explicit resource to load, returns it; otherwise returns null if the test's * context requirement does not include the relevant requirement. + * * @param contextRequirements the context requirements of the test * @param relevantRequirement the relevant context requirement * @param resource the path to the resource @@ -149,4 +235,48 @@ private void bindThreadTargets( } return List.of(contextRequirements).contains(relevantRequirement) ? "" : null; } + + /** + * Starts the given target embedded network from the previous version with any other requested overrides. + * + * @param targetNetwork the target network + * @param overrides the overrides + * @param tssSourceFns the TSS source functions + */ + private void startFromPreviousVersion( + @NonNull final EmbeddedNetwork targetNetwork, + @NonNull final Map overrides, + @NonNull final TssSourceFns tssSourceFns) { + final Map netOverrides = new HashMap<>(overrides); + final var currentVersion = workingDirVersion(); + final var previousVersion = currentVersion + .copyBuilder() + .minor(currentVersion.minor() - 1) + .patch(0) + .pre("") + .build("") + .build(); + netOverrides.put("hedera.services.version", HapiUtils.toString(previousVersion)); + targetNetwork.startWith(netOverrides, tssSourceFns.tssEncryptionKeyFn(), tssSourceFns.tssKeyMaterialFn()); + } + + private FakeState postGenesisStateOf( + @NonNull final EmbeddedNetwork targetNetwork, @NonNull final RestartHapiTest a) { + final var spec = new HapiSpec(SPEC_NAME, new SpecOperation[] {cryptoCreate("genesisAccount")}); + doTargetSpec(spec, targetNetwork); + try { + spec.execute(); + } catch (Throwable e) { + throw new IllegalStateException(e); + } + final SavedStateSpec savedStateSpec; + try { + savedStateSpec = a.savedStateSpec().getDeclaredConstructor().newInstance(); + } catch (Exception e) { + throw new RuntimeException(e); + } + final var state = targetNetwork.embeddedHederaOrThrow().state(); + savedStateSpec.accept(state); + return state; + } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/AbstractLocalNode.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/AbstractLocalNode.java index 92a73fdb9329..c9c5291b7f49 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/AbstractLocalNode.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/AbstractLocalNode.java @@ -16,14 +16,29 @@ package com.hedera.services.bdd.junit.hedera; +import static com.hedera.node.app.info.DiskStartupNetworks.ARCHIVE; +import static com.hedera.node.app.info.DiskStartupNetworks.GENESIS_NETWORK_JSON; +import static com.hedera.node.app.info.DiskStartupNetworks.OVERRIDE_NETWORK_JSON; +import static com.hedera.node.app.info.DiskStartupNetworks.ROUND_DIR_PATTERN; +import static com.hedera.services.bdd.junit.hedera.ExternalPath.DATA_CONFIG_DIR; import static com.hedera.services.bdd.junit.hedera.ExternalPath.UPGRADE_ARTIFACTS_DIR; import static com.hedera.services.bdd.junit.hedera.subprocess.ProcessUtils.conditionFuture; import static com.hedera.services.bdd.junit.hedera.utils.WorkingDirUtils.recreateWorkingDir; import static java.util.Objects.requireNonNull; +import com.hedera.hapi.node.state.roster.RosterEntry; +import com.hedera.node.internal.network.Network; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.hedera.pbj.runtime.io.stream.ReadableStreamingData; import edu.umd.cs.findbugs.annotations.NonNull; +import java.io.IOException; import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Optional; import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import java.util.function.LongFunction; /** * Implementation support for a node that uses a local working directory. @@ -43,9 +58,14 @@ protected AbstractLocalNode(@NonNull final NodeMetadata metadata) { } @Override - public T initWorkingDir(@NonNull final String configTxt) { + public @NonNull T initWorkingDir( + @NonNull final String configTxt, + @NonNull final LongFunction tssEncryptionKeyFn, + @NonNull final Function, Optional> tssKeyMaterialFn) { requireNonNull(configTxt); - recreateWorkingDir(requireNonNull(metadata.workingDir()), configTxt); + requireNonNull(tssEncryptionKeyFn); + requireNonNull(tssKeyMaterialFn); + recreateWorkingDir(requireNonNull(metadata.workingDir()), configTxt, tssEncryptionKeyFn, tssKeyMaterialFn); workingDirInitialized = true; return self(); } @@ -62,9 +82,53 @@ public CompletableFuture mfFuture(@NonNull final MarkerFile markerFile) { return conditionFuture(() -> mfExists(markerFile), () -> MF_BACKOFF_MS); } + @Override + public Optional startupNetwork() { + return getStartupAddressBook(getExternalPath(DATA_CONFIG_DIR)); + } + protected abstract T self(); private boolean mfExists(@NonNull final MarkerFile markerFile) { return Files.exists(getExternalPath(UPGRADE_ARTIFACTS_DIR).resolve(markerFile.fileName())); } + + /** + * Tries to find any startup address book in the given directory or its {@code .archive} subdirectory. + * @param path the path to search + * @return the address book, if found + */ + private Optional getStartupAddressBook(@NonNull final Path path) { + return getStartupAddressBookIn(path).or(() -> getStartupAddressBookIn(path.resolve(ARCHIVE))); + } + + private Optional getStartupAddressBookIn(@NonNull final Path path) { + return getStartupAddressBookAt(path.resolve(GENESIS_NETWORK_JSON)) + .or(() -> getStartupAddressBookAt(path.resolve(OVERRIDE_NETWORK_JSON))) + .or(() -> { + Optional scopedAddressBook = Optional.empty(); + try (final var dirStream = Files.list(path)) { + scopedAddressBook = dirStream + .filter(Files::isDirectory) + .filter(dir -> ROUND_DIR_PATTERN + .matcher(dir.getFileName().toString()) + .matches()) + .map(dir -> getStartupAddressBookAt(dir.resolve(OVERRIDE_NETWORK_JSON))) + .flatMap(Optional::stream) + .findFirst(); + } catch (IOException ignore) { + } + return scopedAddressBook; + }); + } + + private Optional getStartupAddressBookAt(@NonNull final Path path) { + if (Files.exists(path)) { + try (final var fin = Files.newInputStream(path)) { + return Optional.of(Network.JSON.parse(new ReadableStreamingData(fin))); + } catch (Exception ignore) { + } + } + return Optional.empty(); + } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/HederaNetwork.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/HederaNetwork.java index 904f4ddb8ef4..f7d92b22fae1 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/HederaNetwork.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/HederaNetwork.java @@ -18,6 +18,8 @@ import static java.util.Objects.requireNonNull; +import com.hedera.hapi.node.state.roster.RosterEntry; +import com.hedera.pbj.runtime.io.buffer.Bytes; import com.hedera.services.bdd.junit.hedera.remote.RemoteNetwork; import com.hedera.services.bdd.spec.HapiPropertySource; import com.hedera.services.bdd.spec.HapiSpec; @@ -32,6 +34,9 @@ import java.time.Duration; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.LongFunction; /** * A network of Hedera nodes. @@ -156,10 +161,16 @@ default HederaNode getRequiredNode(@NonNull final NodeSelector selector) { void start(); /** - * Starts all nodes in the network with the given overrides. + * Starts all nodes in the network with the given customizations. + * * @param bootstrapOverrides the overrides + * @param tssEncryptionKeyFn the encryption key function + * @param tssKeyMaterialFn the key material function */ - default void startWithOverrides(@NonNull Map bootstrapOverrides) { + default void startWith( + @NonNull final Map bootstrapOverrides, + @NonNull final LongFunction tssEncryptionKeyFn, + @NonNull final Function, Optional> tssKeyMaterialFn) { throw new UnsupportedOperationException(); } 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 7ee5d9e72817..0c2d613c26d1 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 @@ -17,14 +17,21 @@ package com.hedera.services.bdd.junit.hedera; import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.state.roster.RosterEntry; +import com.hedera.node.internal.network.Network; +import com.hedera.pbj.runtime.io.buffer.Bytes; import com.hedera.services.bdd.junit.hedera.subprocess.NodeStatus; import com.hedera.services.bdd.spec.HapiSpec; import com.swirlds.platform.system.status.PlatformStatus; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.nio.file.Path; +import java.util.List; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.LongFunction; public interface HederaNode { /** @@ -80,7 +87,21 @@ public interface HederaNode { * @param configTxt the address book the node should start with * @return this */ - HederaNode initWorkingDir(String configTxt); + default HederaNode initWorkingDir(@NonNull final String configTxt) { + return initWorkingDir(configTxt, nodeId -> Bytes.EMPTY, nodes -> Optional.empty()); + } + + /** + * Initializes the working directory for the node. Must be called before the node is started. + * + * @param configTxt the address book the node should start with + * @return this + */ + @NonNull + HederaNode initWorkingDir( + @NonNull String configTxt, + @NonNull LongFunction tssEncryptionKeyFn, + @NonNull Function, Optional> tssKeyMaterialFn); /** * Starts the node software. @@ -143,4 +164,12 @@ default String hapiSpecInfo() { default boolean dumpThreads() { return false; } + + /** + * If this node's startup assets included a genesis or override address book, returns it. + * @return the node's startup address book, if available + */ + default Optional startupNetwork() { + return Optional.empty(); + } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/TssKeyMaterial.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/TssKeyMaterial.java new file mode 100644 index 000000000000..a0d5377f36bc --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/TssKeyMaterial.java @@ -0,0 +1,31 @@ +/* + * 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; + +import com.hedera.hapi.services.auxiliary.tss.TssMessageTransactionBody; +import com.hedera.node.internal.network.Network; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.List; + +/** + * A summary of all the TSS key material that could be in a {@link Network}. + * + * @param ledgerId the ledger ID + * @param tssMessages the TSS messages + */ +public record TssKeyMaterial(@NonNull Bytes ledgerId, @NonNull List tssMessages) {} 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 1c7626c1a99e..c1d7c8921e82 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 @@ -19,10 +19,11 @@ import static com.hedera.hapi.util.HapiUtils.parseAccount; import static com.hedera.node.app.hapi.utils.CommonPbjConverters.fromPbj; import static com.hedera.services.bdd.junit.hedera.ExternalPath.ADDRESS_BOOK; -import static com.swirlds.platform.roster.RosterRetriever.buildRoster; +import static com.swirlds.platform.roster.RosterUtils.rosterFrom; import static com.swirlds.platform.state.service.PbjConverter.toPbjAddressBook; import static com.swirlds.platform.state.service.schemas.V0540PlatformStateSchema.PLATFORM_STATE_KEY; import static com.swirlds.platform.system.InitTrigger.GENESIS; +import static com.swirlds.platform.system.InitTrigger.RESTART; import static com.swirlds.platform.system.status.PlatformStatus.ACTIVE; import static com.swirlds.platform.system.status.PlatformStatus.FREEZE_COMPLETE; import static java.util.Objects.requireNonNull; @@ -34,12 +35,14 @@ import com.hedera.hapi.node.state.roster.Roster; import com.hedera.hapi.platform.state.PlatformState; import com.hedera.node.app.Hedera; +import com.hedera.node.app.Hedera.TssBaseServiceFactory; +import com.hedera.node.app.ServicesMain; import com.hedera.node.app.fixtures.state.FakeServiceMigrator; import com.hedera.node.app.fixtures.state.FakeServicesRegistry; import com.hedera.node.app.fixtures.state.FakeState; import com.hedera.node.app.info.DiskStartupNetworks; -import com.hedera.node.app.roster.RosterService; import com.hedera.node.app.version.ServicesSoftwareVersion; +import com.hedera.node.internal.network.Network; import com.hedera.pbj.runtime.io.buffer.BufferedData; import com.hedera.pbj.runtime.io.buffer.Bytes; import com.hedera.services.bdd.junit.hedera.embedded.fakes.AbstractFakePlatform; @@ -50,17 +53,14 @@ import com.hederahashgraph.api.proto.java.Timestamp; import com.hederahashgraph.api.proto.java.Transaction; import com.hederahashgraph.api.proto.java.TransactionResponse; +import com.swirlds.base.utility.Pair; import com.swirlds.common.constructable.ConstructableRegistry; import com.swirlds.common.crypto.Hash; import com.swirlds.common.platform.NodeId; -import com.swirlds.config.api.Configuration; -import com.swirlds.config.api.ConfigurationBuilder; -import com.swirlds.config.extensions.sources.SystemEnvironmentConfigSource; -import com.swirlds.config.extensions.sources.SystemPropertiesConfigSource; import com.swirlds.platform.config.legacy.LegacyConfigPropertiesLoader; import com.swirlds.platform.listeners.PlatformStatusChangeNotification; import com.swirlds.platform.state.service.PlatformStateService; -import com.swirlds.platform.state.service.WritableRosterStore; +import com.swirlds.platform.system.InitTrigger; import com.swirlds.platform.system.SoftwareVersion; import com.swirlds.platform.system.address.Address; import com.swirlds.platform.system.address.AddressBook; @@ -106,9 +106,9 @@ public abstract class AbstractEmbeddedHedera implements EmbeddedHedera { protected final Map nodeIds; protected final Map accountIds; - protected final FakeState state = new FakeState(); protected final AccountID defaultNodeAccountId; protected final AddressBook addressBook; + protected final Network network; protected final Roster roster; protected final NodeId defaultNodeId; protected final AtomicInteger nextNano = new AtomicInteger(0); @@ -116,17 +116,33 @@ public abstract class AbstractEmbeddedHedera implements EmbeddedHedera { protected final ServicesSoftwareVersion version; protected final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); + /** + * Non-final because a "saved state" may be provided via {@link EmbeddedHedera#restart(FakeState)}. + */ + protected FakeState state; + /** + * Non-final because the compiler can't tell that the {@link TssBaseServiceFactory} lambda we give the + * {@link Hedera} constructor will always set this (the fake's {@link com.hedera.node.app.tss.TssBaseServiceImpl} + * delegate needs to be constructed from the Hedera instance's {@link com.hedera.node.app.spi.AppContext}). + */ protected FakeTssBaseService tssBaseService; protected AbstractEmbeddedHedera(@NonNull final EmbeddedNode node) { requireNonNull(node); addressBook = loadAddressBook(node.getExternalPath(ADDRESS_BOOK)); - roster = buildRoster(addressBook); - nodeIds = stream(spliteratorUnknownSize(addressBook.iterator(), 0), false) - .collect(toMap(AbstractEmbeddedHedera::accountIdOf, Address::getNodeId)); - accountIds = stream(spliteratorUnknownSize(addressBook.iterator(), 0), false) - .collect(toMap(Address::getNodeId, address -> parseAccount(address.getMemo()))); - defaultNodeId = addressBook.getNodeId(0); + network = node.startupNetwork().orElseThrow(); + roster = rosterFrom(network); + nodeIds = network.nodeMetadata().stream() + .map(metadata -> Pair.of( + fromPbj(metadata.nodeOrThrow().accountIdOrThrow()), + NodeId.of(metadata.rosterEntryOrThrow().nodeId()))) + .collect(toMap(Pair::left, Pair::right)); + accountIds = network.nodeMetadata().stream() + .map(metadata -> Pair.of( + NodeId.of(metadata.rosterEntryOrThrow().nodeId()), + metadata.nodeOrThrow().accountIdOrThrow())) + .collect(toMap(Pair::left, Pair::right)); + defaultNodeId = NodeId.FIRST_NODE_ID; defaultNodeAccountId = fromPbj(accountIds.get(defaultNodeId)); hedera = new Hedera( ConstructableRegistry.getInstance(), @@ -135,25 +151,36 @@ protected AbstractEmbeddedHedera(@NonNull final EmbeddedNode node) { this::now, appContext -> { this.tssBaseService = new FakeTssBaseService(appContext); - return tssBaseService; + return this.tssBaseService; }, - DiskStartupNetworks::new, - NodeId.of(0L)); + DiskStartupNetworks::new); version = (ServicesSoftwareVersion) hedera.getSoftwareVersion(); blockStreamEnabled = hedera.isBlockStreamEnabled(); Runtime.getRuntime().addShutdownHook(new Thread(executorService::shutdownNow)); } @Override - public void start() { - final Configuration configuration = ConfigurationBuilder.create() - .withSource(SystemEnvironmentConfigSource.getInstance()) - .withSource(SystemPropertiesConfigSource.getInstance()) - .autoDiscoverExtensions() - .build(); + public void restart(@NonNull final FakeState state) { + this.state = requireNonNull(state); + start(); + } + @Override + public void start() { + final InitTrigger trigger; + if (state == null) { + trigger = GENESIS; + state = new FakeState(); + } else { + trigger = RESTART; + } hedera.initializeStatesApi( - state, fakePlatform().getContext().getMetrics(), GENESIS, addressBook, configuration); + state, + fakePlatform().getContext().getMetrics(), + trigger, + network, + ServicesMain.buildPlatformConfig(), + addressBook); // TODO - remove this after https://github.com/hashgraph/hedera-services/issues/16552 is done // and we are running all CI tests with the Roster lifecycle enabled @@ -165,12 +192,6 @@ public void start() { .addressBook(toPbjAddressBook(addressBook)) .build()); ((CommittableWritableStates) writableStates).commit(); - if (!hedera.isRosterLifecycleEnabled()) { - final var writableRosterStates = state.getWritableStates(RosterService.NAME); - final WritableRosterStore writableRosterStore = new WritableRosterStore(writableRosterStates); - writableRosterStore.putActiveRoster(buildRoster(addressBook), 0); - ((CommittableWritableStates) writableRosterStates).commit(); - } // --- end of temporary code block --- hedera.setInitialStateHash(FAKE_START_OF_STATE_HASH); @@ -185,6 +206,11 @@ public Hedera hedera() { return hedera; } + @Override + public Roster roster() { + return roster; + } + @Override public void stop() { fakePlatform().notifyListeners(FREEZE_COMPLETE_NOTIFICATION); 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 8b0d1fcd9ffa..5e9458a502ff 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 @@ -115,7 +115,7 @@ private class ConcurrentFakePlatform extends AbstractFakePlatform implements Pla private final ScheduledExecutorService executorService; public ConcurrentFakePlatform(@NonNull final ScheduledExecutorService executorService) { - super(defaultNodeId, addressBook, requireNonNull(executorService)); + super(defaultNodeId, roster, requireNonNull(executorService)); this.executorService = executorService; } @@ -159,7 +159,7 @@ private void handleTransactions() { event.getSoftwareVersion()); }) .toList(); - final var round = new FakeRound(roundNo.getAndIncrement(), roster, consensusEvents); + final var round = new FakeRound(roundNo.getAndIncrement(), requireNonNull(roster), consensusEvents); hedera.handleWorkflow().handleRound(state, round); hedera.onSealConsensusRound(round, state); notifyStateHashed(round.getRoundNum()); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/EmbeddedHedera.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/EmbeddedHedera.java index 883f3187298b..f81f12dc3a49 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/EmbeddedHedera.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/EmbeddedHedera.java @@ -16,6 +16,7 @@ package com.hedera.services.bdd.junit.hedera.embedded; +import com.hedera.hapi.node.state.roster.Roster; import com.hedera.node.app.Hedera; import com.hedera.node.app.fixtures.state.FakeState; import com.hedera.services.bdd.junit.hedera.embedded.fakes.FakeTssBaseService; @@ -37,6 +38,13 @@ public interface EmbeddedHedera { */ void start(); + /** + * Starts the embedded Hedera node from a saved state customized by the given specs. + * + * @param state the state to customize + */ + void restart(@NonNull FakeState state); + /** * Stops the embedded Hedera node. */ @@ -84,6 +92,12 @@ public interface EmbeddedHedera { */ Hedera hedera(); + /** + * Returns the roster of the embedded Hedera node. + * @return the roster of the embedded Hedera node + */ + Roster roster(); + /** * Advances the synthetic time in the embedded Hedera node by a given duration. */ 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 5356af896ad7..0ccaae0d02e4 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 @@ -28,10 +28,14 @@ import static java.util.Objects.requireNonNull; import com.hedera.hapi.node.state.blockrecords.RunningHashes; +import com.hedera.hapi.node.state.roster.RosterEntry; +import com.hedera.node.app.fixtures.state.FakeState; +import com.hedera.pbj.runtime.io.buffer.Bytes; import com.hedera.services.bdd.junit.hedera.AbstractNetwork; import com.hedera.services.bdd.junit.hedera.HederaNetwork; import com.hedera.services.bdd.junit.hedera.HederaNode; import com.hedera.services.bdd.junit.hedera.SystemFunctionalityTarget; +import com.hedera.services.bdd.junit.hedera.TssKeyMaterial; import com.hedera.services.bdd.junit.hedera.utils.WorkingDirUtils; import com.hedera.services.bdd.spec.HapiPropertySource; import com.hedera.services.bdd.spec.TargetNetworkType; @@ -44,7 +48,12 @@ 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.Map; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.LongFunction; import java.util.stream.IntStream; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -97,25 +106,30 @@ public EmbeddedNetwork( this.configTxt = configTxtForLocal(name(), nodes(), 1, 1); } + /** + * Starts the embedded Hedera network from a saved state customized by the given specs. + * + * @param state the state to customize + */ + public void restart(@NonNull final FakeState state, @NonNull final Map bootstrapOverrides) { + requireNonNull(state); + startVia(hedera -> hedera.restart(state), bootstrapOverrides, nodeId -> Bytes.EMPTY, nodes -> Optional.empty()); + } + @Override public void start() { - startWithOverrides(emptyMap()); + startWith(emptyMap(), nodeId -> Bytes.EMPTY, nodes -> Optional.empty()); } @Override - public void startWithOverrides(@NonNull final Map bootstrapOverrides) { + public void startWith( + @NonNull final Map bootstrapOverrides, + @NonNull final LongFunction tssEncryptionKeyFn, + @NonNull final Function, Optional> tssKeyMaterialFn) { requireNonNull(bootstrapOverrides); - // Initialize the working directory - embeddedNode.initWorkingDir(configTxt); - if (!bootstrapOverrides.isEmpty()) { - updateBootstrapProperties(embeddedNode.getExternalPath(APPLICATION_PROPERTIES), bootstrapOverrides); - } - embeddedNode.start(); - // Start the embedded Hedera "network" - embeddedHedera = switch (mode) { - case REPEATABLE -> new RepeatableEmbeddedHedera(embeddedNode); - case CONCURRENT -> new ConcurrentEmbeddedHedera(embeddedNode);}; - embeddedHedera.start(); + requireNonNull(tssEncryptionKeyFn); + requireNonNull(tssKeyMaterialFn); + startVia(EmbeddedHedera::start, bootstrapOverrides, tssEncryptionKeyFn, tssKeyMaterialFn); } @Override @@ -190,4 +204,22 @@ public EmbeddedMode mode() { protected HapiPropertySource networkOverrides() { return WorkingDirUtils.hapiTestStartupProperties(); } + + private void startVia( + @NonNull final Consumer start, + @NonNull final Map bootstrapOverrides, + @NonNull final LongFunction tssEncryptionKeyFn, + @NonNull final Function, Optional> tssKeyMaterialFn) { + // Initialize the working directory + embeddedNode.initWorkingDir(configTxt, tssEncryptionKeyFn, tssKeyMaterialFn); + if (!bootstrapOverrides.isEmpty()) { + updateBootstrapProperties(embeddedNode.getExternalPath(APPLICATION_PROPERTIES), bootstrapOverrides); + } + embeddedNode.start(); + // Start the embedded Hedera "network" + embeddedHedera = switch (mode) { + case REPEATABLE -> new RepeatableEmbeddedHedera(embeddedNode); + case CONCURRENT -> new ConcurrentEmbeddedHedera(embeddedNode);}; + start.accept(embeddedHedera); + } } 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 e6a250279da6..db237fc3b353 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.common.platform.NodeId; import com.swirlds.platform.system.Platform; import com.swirlds.platform.system.Round; -import com.swirlds.platform.system.address.AddressBook; import com.swirlds.platform.system.events.ConsensusEvent; import edu.umd.cs.findbugs.annotations.NonNull; import java.time.Duration; @@ -64,7 +63,7 @@ public class RepeatableEmbeddedHedera extends AbstractEmbeddedHedera implements public RepeatableEmbeddedHedera(@NonNull final EmbeddedNode node) { super(node); - platform = new SynchronousFakePlatform(defaultNodeId, addressBook, executorService); + platform = new SynchronousFakePlatform(defaultNodeId, executorService); } @Override @@ -161,10 +160,8 @@ private class SynchronousFakePlatform extends AbstractFakePlatform implements Pl private FakeEvent lastCreatedEvent; public SynchronousFakePlatform( - @NonNull NodeId selfId, - @NonNull AddressBook addressBook, - @NonNull ScheduledExecutorService executorService) { - super(selfId, addressBook, executorService); + @NonNull final NodeId selfId, @NonNull final ScheduledExecutorService executorService) { + super(selfId, roster, executorService); } @Override diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/fakes/AbstractFakePlatform.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/fakes/AbstractFakePlatform.java index b1cffb9094d4..7c620dc29064 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/fakes/AbstractFakePlatform.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/fakes/AbstractFakePlatform.java @@ -26,10 +26,8 @@ import com.swirlds.common.platform.NodeId; import com.swirlds.common.utility.AutoCloseableWrapper; import com.swirlds.platform.listeners.PlatformStatusChangeNotification; -import com.swirlds.platform.roster.RosterRetriever; import com.swirlds.platform.system.Platform; import com.swirlds.platform.system.SwirldState; -import com.swirlds.platform.system.address.AddressBook; import edu.umd.cs.findbugs.annotations.NonNull; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.atomic.AtomicLong; @@ -42,19 +40,17 @@ public abstract class AbstractFakePlatform implements Platform { protected final AtomicLong consensusOrder = new AtomicLong(1); private final NodeId selfId; - private final AddressBook addressBook; private final Roster roster; private final PlatformContext platformContext; private final FakeNotificationEngine notificationEngine = new FakeNotificationEngine(); public AbstractFakePlatform( @NonNull final NodeId selfId, - @NonNull final AddressBook addressBook, + @NonNull final Roster roster, @NonNull final ScheduledExecutorService executorService) { requireNonNull(executorService); this.selfId = requireNonNull(selfId); - this.addressBook = requireNonNull(addressBook); - this.roster = RosterRetriever.buildRoster(addressBook); + this.roster = requireNonNull(roster); platformContext = new FakePlatformContext(selfId, executorService); } 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 0f714498b8d7..6edda8d6752c 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 @@ -24,6 +24,7 @@ import com.hedera.hapi.node.state.roster.Roster; import com.hedera.node.app.services.ServiceMigrator; import com.hedera.node.app.spi.AppContext; +import com.hedera.node.app.spi.metrics.StoreMetricsService; import com.hedera.node.app.spi.workflows.HandleContext; import com.hedera.node.app.tss.TssBaseService; import com.hedera.node.app.tss.TssBaseServiceImpl; @@ -253,8 +254,8 @@ public void regenerateKeyMaterial(@NonNull final State state) { } @Override - public void generateParticipantDirectory(@NonNull final State state) { - delegate.generateParticipantDirectory(state); + public void ensureParticipantDirectoryKnown(@NonNull final State state) { + delegate.ensureParticipantDirectoryKnown(state); } @Override @@ -271,7 +272,11 @@ public TssMessage getTssMessageFromBytes(Bytes wrap, TssParticipantDirectory dir } @Override - public void manageTssStatus(final State state) { - delegate.manageTssStatus(state); + public void manageTssStatus( + final State state, + final boolean isStakePeriodBoundary, + final Instant lastUsedConsensusNow, + final StoreMetricsService storeMetricsService) { + delegate.manageTssStatus(state, isStakePeriodBoundary, lastUsedConsensusNow, storeMetricsService); } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/remote/RemoteNode.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/remote/RemoteNode.java index ed7a2bd578c8..15f3c8510380 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/remote/RemoteNode.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/remote/RemoteNode.java @@ -16,18 +16,25 @@ package com.hedera.services.bdd.junit.hedera.remote; +import com.hedera.hapi.node.state.roster.RosterEntry; +import com.hedera.pbj.runtime.io.buffer.Bytes; import com.hedera.services.bdd.junit.hedera.AbstractNode; import com.hedera.services.bdd.junit.hedera.ExternalPath; import com.hedera.services.bdd.junit.hedera.HederaNode; import com.hedera.services.bdd.junit.hedera.MarkerFile; import com.hedera.services.bdd.junit.hedera.NodeMetadata; +import com.hedera.services.bdd.junit.hedera.TssKeyMaterial; import com.hedera.services.bdd.junit.hedera.subprocess.NodeStatus; import com.swirlds.platform.system.status.PlatformStatus; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.nio.file.Path; +import java.util.List; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.LongFunction; public class RemoteNode extends AbstractNode implements HederaNode { public RemoteNode(@NonNull final NodeMetadata metadata) { @@ -44,6 +51,15 @@ public HederaNode initWorkingDir(@NonNull final String configTxt) { throw new UnsupportedOperationException("Cannot initialize a remote node's working directory"); } + @NonNull + @Override + public HederaNode initWorkingDir( + @NonNull final String configTxt, + @NonNull final LongFunction tssEncryptionKeyFn, + @NonNull final Function, Optional> tssKeyMaterialFn) { + throw new UnsupportedOperationException("Cannot initialize a remote node's working directory"); + } + @Override public HederaNode start() { // No-op, remote nodes must already be running 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 94dec3ce59dc..2fbeaef48572 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 @@ -211,6 +211,10 @@ public void terminate() { @Override public void awaitReady(@NonNull final Duration timeout) { if (ready.get() == null) { + log.info( + "Newly waiting for network '{}' to be ready in thread '{}'", + name(), + Thread.currentThread().getName()); final var deferredRun = new DeferredRun(() -> { AssertionError error = null; var retries = MAX_PORT_REASSIGNMENTS; 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 93fe96ce6a62..7806c39c7f87 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 @@ -32,7 +32,6 @@ import static com.hedera.services.bdd.junit.hedera.subprocess.StatusLookupAttempt.newLogAttempt; import static com.hedera.services.bdd.junit.hedera.utils.WorkingDirUtils.ERROR_REDIRECT_FILE; import static com.hedera.services.bdd.junit.hedera.utils.WorkingDirUtils.OUTPUT_DIR; -import static com.hedera.services.bdd.junit.hedera.utils.WorkingDirUtils.recreateWorkingDir; import static com.hedera.services.bdd.spec.HapiPropertySource.asAccount; import static com.swirlds.platform.system.status.PlatformStatus.ACTIVE; import static java.util.Objects.requireNonNull; @@ -106,13 +105,6 @@ public SubProcessNode( requireNonNull(Hedera.class); } - @Override - public SubProcessNode initWorkingDir(@NonNull final String configTxt) { - recreateWorkingDir(requireNonNull(metadata.workingDir()), configTxt); - workingDirInitialized = true; - return this; - } - @Override public SubProcessNode start() { return startWithConfigVersion(LifecycleTest.CURRENT_CONFIG_VERSION.get()); 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 676890449498..0d2648889b2b 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 @@ -16,32 +16,103 @@ package com.hedera.services.bdd.junit.hedera.utils; +import static com.hedera.services.bdd.junit.hedera.embedded.fakes.FakeTssLibrary.FAKE_LEDGER_ID; import static com.hedera.services.bdd.junit.hedera.utils.WorkingDirUtils.workingDirFor; import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.toMap; import static java.util.stream.StreamSupport.stream; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.protobuf.ByteString; +import com.hedera.cryptography.bls.BlsPublicKey; import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.state.roster.Roster; +import com.hedera.hapi.services.auxiliary.tss.TssMessageTransactionBody; +import com.hedera.node.app.tss.api.FakeGroupElement; +import com.hedera.node.app.tss.handlers.TssUtils; +import com.hedera.pbj.runtime.io.buffer.Bytes; import com.hedera.services.bdd.junit.hedera.HederaNode; import com.hedera.services.bdd.junit.hedera.NodeMetadata; +import com.hedera.services.bdd.junit.hedera.TssKeyMaterial; +import com.hedera.services.bdd.junit.hedera.embedded.fakes.FakeTssLibrary; import com.hederahashgraph.api.proto.java.ServiceEndpoint; import com.swirlds.common.platform.NodeId; +import com.swirlds.platform.roster.RosterUtils; import com.swirlds.platform.system.address.Address; import com.swirlds.platform.system.address.AddressBook; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; +import java.io.IOException; +import java.math.BigInteger; import java.nio.file.Path; import java.util.List; +import java.util.Map; +import java.util.function.Function; import java.util.regex.Pattern; +import java.util.stream.IntStream; +import java.util.stream.LongStream; import java.util.stream.Stream; /** * Utility class for generating an address book configuration file. */ public class AddressBookUtils { + private static Map TEST_GOSSIP_X509_CERTS; + public static final long CLASSIC_FIRST_NODE_ACCOUNT_NUM = 3; public static final String[] CLASSIC_NODE_NAMES = new String[] {"node1", "node2", "node3", "node4", "node5", "node6", "node7", "node8"}; + // TODO - replace with real encryption keys + public static final Map CLASSIC_ENCRYPTION_KEYS = LongStream.range(0, CLASSIC_NODE_NAMES.length) + .boxed() + .collect(toMap(Function.identity(), i -> Bytes.fromHex("aa".repeat(i.intValue() + 1)))); + // TODO - make this parameterizable + public static final int CLASSIC_MAX_SHARES_PER_NODE = 3; + // TODO - generate real shares, encode message using the real encryption keys + public static final Function CLASSIC_KEY_MATERIAL_GENERATOR = roster -> { + final var directory = TssUtils.computeParticipantDirectory( + roster, + CLASSIC_MAX_SHARES_PER_NODE, + nodeId -> new BlsPublicKey( + new FakeGroupElement(new BigInteger( + CLASSIC_ENCRYPTION_KEYS.get(nodeId).toByteArray())), + TssUtils.SIGNATURE_SCHEMA)); + final var rosterHash = RosterUtils.hash(roster).getBytes(); + final var tssMessageOps = IntStream.range(0, directory.getThreshold()) + .mapToObj(i -> TssMessageTransactionBody.newBuilder() + .shareIndex(i + 1L) + .sourceRosterHash(Bytes.EMPTY) + .targetRosterHash(rosterHash) + .tssMessage(Bytes.wrap(FakeTssLibrary.validMessage(i).toBytes())) + .build()) + .toList(); + return new TssKeyMaterial(Bytes.wrap(FAKE_LEDGER_ID.toBytes()), tssMessageOps); + }; + + /** + * Returns the ASN.1 DER encoding of the X.509 certificate the platform generates for the given node id + * in test environments. + * @param nodeId the node id + * @return the ASN.1 DER encoding of the X.509 certificate + */ + @SuppressWarnings("unchecked") + public static Bytes testCertFor(final long nodeId) { + if (TEST_GOSSIP_X509_CERTS == null) { + try { + TEST_GOSSIP_X509_CERTS = ((Map) new ObjectMapper() + .readValue( + AddressBookUtils.class + .getClassLoader() + .getResourceAsStream("hapi-test-gossip-certs.json"), + Map.class)) + .entrySet().stream() + .collect(toMap(e -> Long.parseLong(e.getKey()), e -> Bytes.fromBase64(e.getValue()))); + } catch (IOException e) { + throw new IllegalStateException("Could not load gossip certs", e); + } + } + return TEST_GOSSIP_X509_CERTS.get(nodeId); + } private AddressBookUtils() { throw new UnsupportedOperationException("Utility Class"); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/utils/WorkingDirUtils.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/utils/WorkingDirUtils.java index a3cc4250b098..c044b7bea2c0 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/utils/WorkingDirUtils.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/utils/WorkingDirUtils.java @@ -16,10 +16,23 @@ package com.hedera.services.bdd.junit.hedera.utils; +import static com.hedera.node.app.hapi.utils.CommonPbjConverters.toPbj; +import static com.hedera.node.app.info.DiskStartupNetworks.GENESIS_NETWORK_JSON; import static java.util.Objects.requireNonNull; import static java.util.Spliterators.spliteratorUnknownSize; import static java.util.stream.StreamSupport.stream; +import com.hedera.hapi.node.base.Key; +import com.hedera.hapi.node.base.SemanticVersion; +import com.hedera.hapi.node.base.ServiceEndpoint; +import com.hedera.hapi.node.state.addressbook.Node; +import com.hedera.hapi.node.state.roster.RosterEntry; +import com.hedera.node.config.converter.SemanticVersionConverter; +import com.hedera.node.internal.network.Network; +import com.hedera.node.internal.network.NodeMetadata; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.hedera.services.bdd.junit.hedera.TssKeyMaterial; +import com.hedera.services.bdd.spec.HapiPropertySource; import com.hedera.services.bdd.spec.props.JutilPropertySource; import com.swirlds.platform.config.legacy.LegacyConfigPropertiesLoader; import com.swirlds.platform.crypto.CryptoStatic; @@ -36,11 +49,15 @@ import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.security.cert.X509Certificate; +import java.util.Arrays; import java.util.Comparator; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Properties; import java.util.Random; +import java.util.function.Function; +import java.util.function.LongFunction; import java.util.stream.Stream; public class WorkingDirUtils { @@ -100,8 +117,19 @@ public static Path workingDirFor(final long nodeId, @Nullable String scope) { * * @param workingDir the path to the working directory * @param configTxt the contents of the config.txt file + * @param tssEncryptionKeyFn a function that returns the TSS encryption key for a given node ID + * @param tssKeyMaterialFn a function that returns the TSS key material for the network, if available */ - public static void recreateWorkingDir(@NonNull final Path workingDir, @NonNull final String configTxt) { + public static void recreateWorkingDir( + @NonNull final Path workingDir, + @NonNull final String configTxt, + @NonNull final LongFunction tssEncryptionKeyFn, + @NonNull final Function, Optional> tssKeyMaterialFn) { + requireNonNull(workingDir); + requireNonNull(configTxt); + requireNonNull(tssEncryptionKeyFn); + requireNonNull(tssKeyMaterialFn); + // Clean up any existing directory structure rm(workingDir); // Initialize the data folders @@ -110,8 +138,12 @@ public static void recreateWorkingDir(@NonNull final Path workingDir, @NonNull f // Initialize the current upgrade folder createDirectoriesUnchecked( workingDir.resolve(DATA_DIR).resolve(UPGRADE_DIR).resolve(CURRENT_DIR)); - // Write the address book (config.txt) + // Write the address book (config.txt) and genesis network (genesis-network.json) files writeStringUnchecked(workingDir.resolve(CONFIG_TXT), configTxt); + final var network = networkFrom(configTxt, tssEncryptionKeyFn, tssKeyMaterialFn); + final var networkJson = Network.JSON.toJSON(network); + writeStringUnchecked( + workingDir.resolve(DATA_DIR).resolve(CONFIG_FOLDER).resolve(GENESIS_NETWORK_JSON), networkJson); // Copy the bootstrap assets into the working directory copyBootstrapAssets(bootstrapAssetsLoc(), workingDir); // Update the log4j2.xml file with the correct output directory @@ -132,6 +164,7 @@ public static void updateUpgradeArtifactsProperty( /** * Updates the given key/value property override at the given location + * * @param propertiesPath the path to the properties file * @param overrides the key/value property overrides */ @@ -160,6 +193,19 @@ public static JutilPropertySource hapiTestStartupProperties() { return new JutilPropertySource(bootstrapAssetsLoc().resolve(APPLICATION_PROPERTIES)); } + /** + * Returns the version in the project's {@code version.txt} file. + * + * @return the version + */ + public @NonNull static SemanticVersion workingDirVersion() { + final var loc = Paths.get(System.getProperty("user.dir")).endsWith("hedera-services") + ? "version.txt" + : "../version.txt"; + final var versionLiteral = readStringUnchecked(Paths.get(loc)).trim(); + return requireNonNull(new SemanticVersionConverter().convert(versionLiteral)); + } + private static Path bootstrapAssetsLoc() { return Paths.get(System.getProperty("user.dir")).endsWith("hedera-services") ? Path.of(PROJECT_BOOTSTRAP_ASSETS_LOC) @@ -353,4 +399,49 @@ public static AddressBook loadAddressBookWithDeterministicCerts(@NonNull final P throw new RuntimeException("Error generating keys and certs", e); } } + + private static Network networkFrom( + @NonNull final String configTxt, + @NonNull final LongFunction tssEncryptionKeyFn, + @NonNull final Function, Optional> tssKeyMaterialFn) { + final var nodeMetadata = Arrays.stream(configTxt.split("\n")) + .filter(line -> line.contains("address, ")) + .map(line -> { + final var parts = line.split(", "); + final long nodeId = Long.parseLong(parts[1]); + final long weight = Long.parseLong(parts[4]); + final var gossipEndpoints = + List.of(endpointFrom(parts[5], parts[6]), endpointFrom(parts[7], parts[8])); + final var cert = AddressBookUtils.testCertFor(nodeId); + return NodeMetadata.newBuilder() + .rosterEntry(new RosterEntry(nodeId, weight, cert, gossipEndpoints)) + .node(new Node( + nodeId, + toPbj(HapiPropertySource.asAccount(parts[9])), + "node" + (nodeId + 1), + gossipEndpoints, + List.of(), + cert, + // The gRPC certificate hash is irrelevant for PR checks + Bytes.EMPTY, + weight, + false, + // TODO - Use the real admin key + Key.DEFAULT)) + .tssEncryptionKey(tssEncryptionKeyFn.apply(nodeId)) + .build(); + }) + .toList(); + final var roster = nodeMetadata.stream().map(NodeMetadata::rosterEntry).toList(); + final var tssKeyMaterial = tssKeyMaterialFn.apply(roster); + return Network.newBuilder() + .ledgerId(tssKeyMaterial.map(TssKeyMaterial::ledgerId).orElse(Bytes.EMPTY)) + .tssMessages(tssKeyMaterial.map(TssKeyMaterial::tssMessages).orElse(List.of())) + .nodeMetadata(nodeMetadata) + .build(); + } + + private static ServiceEndpoint endpointFrom(@NonNull final String hostLiteral, @NonNull final String portLiteral) { + return HapiPropertySource.asServiceEndpoint(hostLiteral + ":" + portLiteral); + } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/restart/NoopSavedStateSpec.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/restart/NoopSavedStateSpec.java new file mode 100644 index 000000000000..b94a597e3aae --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/restart/NoopSavedStateSpec.java @@ -0,0 +1,32 @@ +/* + * 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.restart; + +import static java.util.Objects.requireNonNull; + +import com.hedera.node.app.fixtures.state.FakeState; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * A no-op implementation of a {@link SavedStateSpec}. + */ +public class NoopSavedStateSpec implements SavedStateSpec { + @Override + public void accept(@NonNull final FakeState state) { + requireNonNull(state); + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/restart/RestartHapiTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/restart/RestartHapiTest.java new file mode 100644 index 000000000000..ba7726add487 --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/restart/RestartHapiTest.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.hedera.services.bdd.junit.restart; + +import static com.hedera.services.bdd.junit.TestTags.ONLY_REPEATABLE; +import static org.junit.jupiter.api.parallel.ExecutionMode.SAME_THREAD; + +import com.hedera.node.app.fixtures.state.FakeState; +import com.hedera.services.bdd.junit.ConfigOverride; +import com.hedera.services.bdd.junit.HapiTest; +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.Execution; + +/** + * Annotation for a repeatable {@link HapiTest} that exercises the restart phase by creating a new embedded network + * separate from the shared network. The type of restart scenario is distinguished by the presence or absence of two + * on-disk assets: A saved state and an override network roster. + *

    + * If a saved state is present, it may be from the previous software version or the current version; and as of release + * {@code 0.57}, it may or may not have a TSS ledger id already in state. (But note that after the production deployment + * of TSS, every saved state must already have a TSS ledger id.) + *

    + * If an override network roster is present, it similarly may or may not come have a pre-generated TSS ledger id. (Even + * after all production networks have TSS enabled, it may be occasionally useful to be able to transplant just roster + * information and have the target network generate a new ledger id.) + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@TestFactory +@ExtendWith({NetworkTargetingExtension.class, SpecNamingExtension.class}) +@Execution(SAME_THREAD) +@Tag(ONLY_REPEATABLE) +public @interface RestartHapiTest { + /** + * The type of restart being tested. + */ + RestartType restartType() default RestartType.GENESIS; + + /** + * Any overrides that should be present when creating the setup state before restart. + */ + ConfigOverride[] setupOverrides() default {}; + + /** + * Any overrides that should be present at restart. + */ + ConfigOverride[] restartOverrides() default {}; + + /** + * The type of startup assets that should be present on disk when creating the setup state before restart. + */ + StartupAssets setupAssets() default StartupAssets.NONE; + + /** + * The type of startup assets that should be present on disk for the test. + */ + StartupAssets restartAssets() default StartupAssets.NONE; + + /** + * The type of saved state spec that should be used to customize the state of the {@link FakeState} when + * using {@link RestartType#SAME_VERSION} or {@link RestartType#UPGRADE_BOUNDARY}. + */ + Class savedStateSpec() default NoopSavedStateSpec.class; +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/restart/RestartType.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/restart/RestartType.java new file mode 100644 index 000000000000..dd30fb606f8a --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/restart/RestartType.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.services.bdd.junit.restart; + +/** + * The types of restarts to be covered by a {@link RestartHapiTest}. + */ +public enum RestartType { + /** + * The "restart" is from genesis. + */ + GENESIS, + /** + * The restart uses the same software version as the saved state. + */ + SAME_VERSION, + /** + * The restart uses a later software version than the saved state. + */ + UPGRADE_BOUNDARY, +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/restart/SavedStateSpec.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/restart/SavedStateSpec.java new file mode 100644 index 000000000000..83e4892c59c3 --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/restart/SavedStateSpec.java @@ -0,0 +1,26 @@ +/* + * 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.restart; + +import com.hedera.node.app.fixtures.state.FakeState; +import java.util.function.Consumer; + +/** + * A functional interface to customize the state of a {@link FakeState} object when setting up a {@link RestartHapiTest}. + */ +@FunctionalInterface +public interface SavedStateSpec extends Consumer {} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/restart/StartupAssets.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/restart/StartupAssets.java new file mode 100644 index 000000000000..3ae167fc95ae --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/restart/StartupAssets.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.services.bdd.junit.restart; + +/** + * The types of startup assets available for a {@link RestartHapiTest}. + */ +public enum StartupAssets { + /** + * No network override is present. + */ + NONE, + /** + * A network override with only the network roster is present. + */ + ROSTER_ONLY, + /** + * A network override with both the roster and the encryption keys are present. + */ + ROSTER_AND_ENCRYPTION_KEYS, + /** + * A network override with both the network roster and all TSS key material, + * including the ledger id, is present. + */ + ROSTER_AND_FULL_TSS_KEY_MATERIAL, +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/translators/BaseTranslator.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/translators/BaseTranslator.java index c87a9a52e843..7180760d486f 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/translators/BaseTranslator.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/translators/BaseTranslator.java @@ -22,6 +22,7 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.SUCCESS; import static com.hedera.hapi.util.HapiUtils.asInstant; import static com.hedera.hapi.util.HapiUtils.asTimestamp; +import static com.hedera.node.app.service.schedule.impl.handlers.HandlerUtility.scheduledTxnIdFrom; import static com.hedera.node.config.types.EntityType.ACCOUNT; import static com.hedera.node.config.types.EntityType.FILE; import static com.hedera.node.config.types.EntityType.NODE; @@ -68,6 +69,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -89,6 +91,8 @@ public class BaseTranslator { private ExchangeRateSet activeRates; private final Map totalSupplies = new HashMap<>(); private final Map tokenTypes = new HashMap<>(); + private final Map scheduleRefs = new HashMap<>(); + private final Map scheduleTxnIds = new HashMap<>(); private final Set knownAssociations = new HashSet<>(); private final Map pendingAirdrops = new HashMap<>(); @@ -98,11 +102,11 @@ public class BaseTranslator { private long prevHighestKnownEntityNum = 0L; private Instant userTimestamp; - private ScheduleID scheduleRef; private final List sidecarRecords = new ArrayList<>(); private final Map numMints = new HashMap<>(); private final Map> highestPutSerialNos = new HashMap<>(); private final Map> nextCreatedNums = new EnumMap<>(EntityType.class); + private final Set purgedScheduleIds = new HashSet<>(); /** * Defines how a translator specifies details of a translated transaction record. @@ -169,7 +173,7 @@ public void prepareForUnit(@NonNull final BlockTransactionalUnit unit) { highestPutSerialNos.clear(); nextCreatedNums.clear(); sidecarRecords.clear(); - scheduleRef = null; + purgedScheduleIds.clear(); scanUnit(unit); nextCreatedNums.values().forEach(list -> { final Set distinctNums = Set.copyOf(list); @@ -192,6 +196,13 @@ public void prepareForUnit(@NonNull final BlockTransactionalUnit unit) { nextCreatedNums.values().stream().mapToLong(List::getLast).max().orElse(highestKnownEntityNum); } + /** + * Finishes the ongoing transactional unit, purging any schedules that were deleted. + */ + public void finishLastUnit() { + purgedScheduleIds.forEach(scheduleId -> scheduleRefs.remove(scheduleTxnIds.remove(scheduleId))); + } + /** * Determines if the given number was created in the ongoing transactional unit. * @@ -353,8 +364,9 @@ public SingleTransactionRecord recordFrom(@NonNull final BlockTransactionParts p final var output = parts.callContractOutputOrThrow(); recordBuilder.contractCallResult(output.contractCallResultOrThrow()); } + // If this transaction was executed by virtue of being scheduled, set its schedule ref if (parts.transactionIdOrThrow().scheduled()) { - recordBuilder.scheduleRef(scheduleRefOrThrow()); + Optional.ofNullable(scheduleRefs.get(parts.transactionIdOrThrow())).ifPresent(recordBuilder::scheduleRef); } return new SingleTransactionRecord( parts.transactionParts().wrapper(), @@ -386,22 +398,13 @@ public ExchangeRateSet activeRates() { return activeRates; } - /** - * Returns the modified schedule id for the ongoing transactional unit. - * - * @return the modified schedule id - */ - public @NonNull ScheduleID scheduleRefOrThrow() { - return requireNonNull(scheduleRef); - } - private void scanUnit(@NonNull final BlockTransactionalUnit unit) { unit.stateChanges().forEach(stateChange -> { if (stateChange.hasMapDelete()) { final var mapDelete = stateChange.mapDeleteOrThrow(); final var key = mapDelete.keyOrThrow(); if (key.hasScheduleIdKey()) { - scheduleRef = key.scheduleIdKeyOrThrow(); + purgedScheduleIds.add(key.scheduleIdKeyOrThrow()); } } else if (stateChange.hasMapUpdate()) { final var mapUpdate = stateChange.mapUpdateOrThrow(); @@ -439,7 +442,12 @@ private void scanUnit(@NonNull final BlockTransactionalUnit unit) { .computeIfAbsent(SCHEDULE, ignore -> new LinkedList<>()) .add(num); } - scheduleRef = key.scheduleIdKeyOrThrow(); + final var schedule = mapUpdate.valueOrThrow().scheduleValueOrThrow(); + final var scheduleId = key.scheduleIdKeyOrThrow(); + final var scheduledTxnId = scheduledTxnIdFrom( + schedule.originalCreateTransactionOrThrow().transactionIDOrThrow()); + scheduleRefs.put(scheduledTxnId, scheduleId); + scheduleTxnIds.put(scheduleId, scheduledTxnId); } else if (key.hasAccountIdKey()) { final var num = key.accountIdKeyOrThrow().accountNumOrThrow(); if (num > highestKnownEntityNum) { diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/translators/BlockTransactionalUnitTranslator.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/translators/BlockTransactionalUnitTranslator.java index ee6c45595fdd..e9d6217bc1bf 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/translators/BlockTransactionalUnitTranslator.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/translators/BlockTransactionalUnitTranslator.java @@ -215,6 +215,7 @@ public List translate(@NonNull final BlockTransactional translatedRecords.add(translation); } } + baseTranslator.finishLastUnit(); return translatedRecords; } } 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 4d6b90785b71..0ffecdb538a8 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 @@ -20,6 +20,7 @@ import static com.hedera.node.app.hapi.utils.CommonUtils.noThrowSha384HashOf; import static com.hedera.node.app.hapi.utils.CommonUtils.sha384DigestOrThrow; import static com.hedera.services.bdd.junit.hedera.ExternalPath.APPLICATION_PROPERTIES; +import static com.hedera.services.bdd.junit.hedera.ExternalPath.DATA_CONFIG_DIR; import static com.hedera.services.bdd.junit.hedera.ExternalPath.SAVED_STATES_DIR; import static com.hedera.services.bdd.junit.hedera.ExternalPath.SWIRLDS_LOG; import static com.hedera.services.bdd.junit.hedera.NodeSelector.byNodeId; @@ -28,7 +29,6 @@ import static com.hedera.services.bdd.junit.hedera.utils.WorkingDirUtils.workingDirFor; import static com.hedera.services.bdd.junit.support.validators.block.ChildHashUtils.hashesByName; import static com.hedera.services.bdd.spec.TargetNetworkType.SUBPROCESS_NETWORK; -import static com.swirlds.platform.state.GenesisStateBuilder.initGenesisPlatformState; import static java.util.Objects.requireNonNull; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -47,47 +47,29 @@ import com.hedera.hapi.node.state.primitives.ProtoBytes; import com.hedera.hapi.node.state.primitives.ProtoLong; import com.hedera.hapi.node.state.primitives.ProtoString; -import com.hedera.node.app.Hedera; +import com.hedera.node.app.ServicesMain; import com.hedera.node.app.blocks.BlockStreamManager; import com.hedera.node.app.blocks.StreamingTreeHasher; import com.hedera.node.app.blocks.impl.NaiveStreamingTreeHasher; import com.hedera.node.app.config.BootstrapConfigProviderImpl; import com.hedera.node.app.info.DiskStartupNetworks; -import com.hedera.node.app.services.OrderedServiceMigrator; -import com.hedera.node.app.services.ServicesRegistryImpl; -import com.hedera.node.app.tss.TssBaseServiceImpl; -import com.hedera.node.app.tss.TssLibraryImpl; -import com.hedera.node.app.version.ServicesSoftwareVersion; -import com.hedera.node.config.converter.BytesConverter; -import com.hedera.node.config.data.HederaConfig; import com.hedera.node.config.data.VersionConfig; import com.hedera.pbj.runtime.io.buffer.Bytes; import com.hedera.services.bdd.junit.hedera.subprocess.SubProcessNetwork; import com.hedera.services.bdd.junit.support.BlockStreamAccess; import com.hedera.services.bdd.junit.support.BlockStreamValidator; import com.hedera.services.bdd.spec.HapiSpec; -import com.swirlds.common.config.StateCommonConfig; -import com.swirlds.common.constructable.ConstructableRegistry; import com.swirlds.common.crypto.Hash; -import com.swirlds.common.crypto.config.CryptoConfig; -import com.swirlds.common.io.config.TemporaryFileConfig; import com.swirlds.common.merkle.crypto.MerkleCryptoFactory; import com.swirlds.common.merkle.crypto.MerkleCryptography; import com.swirlds.common.merkle.utility.MerkleTreeVisualizer; -import com.swirlds.common.metrics.config.MetricsConfig; import com.swirlds.common.metrics.noop.NoOpMetrics; import com.swirlds.common.platform.NodeId; -import com.swirlds.config.api.Configuration; -import com.swirlds.config.api.ConfigurationBuilder; -import com.swirlds.merkledb.config.MerkleDbConfig; -import com.swirlds.platform.config.BasicConfig; -import com.swirlds.platform.config.TransactionConfig; import com.swirlds.platform.state.PlatformMerkleStateRoot; import com.swirlds.platform.system.InitTrigger; import com.swirlds.state.lifecycle.Service; import com.swirlds.state.merkle.MerkleStateRoot; import com.swirlds.state.spi.CommittableWritableStates; -import com.swirlds.virtualmap.config.VirtualMapConfig; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.io.IOException; @@ -96,13 +78,12 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.time.InstantSource; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.SplittableRandom; import java.util.TreeMap; -import java.util.concurrent.ForkJoinPool; import java.util.regex.Pattern; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -114,20 +95,26 @@ */ public class StateChangesValidator implements BlockStreamValidator { private static final Logger logger = LogManager.getLogger(StateChangesValidator.class); + private static final SplittableRandom RANDOM = new SplittableRandom(System.currentTimeMillis()); private static final MerkleCryptography CRYPTO = MerkleCryptoFactory.getInstance(); private static final int HASH_SIZE = 48; private static final int VISUALIZATION_HASH_DEPTH = 5; + /** + * The probability that the validator will verify an intermediate block proof; we always verify the first and last. + */ + private static final double PROOF_VERIFICATION_PROB = 0.05; + private static final Pattern NUMBER_PATTERN = Pattern.compile("\\d+"); private static final Pattern CHILD_STATE_PATTERN = Pattern.compile("\\s+\\d+ \\w+\\s+(\\S+)\\s+.+\\s+(.+)"); + private final Hash genesisStateHash; private final Path pathToNode0SwirldsLog; private final Bytes expectedRootHash; private final Set servicesWritten = new HashSet<>(); private final StateChangesSummary stateChangesSummary = new StateChangesSummary(new TreeMap<>()); private PlatformMerkleStateRoot state; - private Hash genesisStateHash; public static void main(String[] args) { final var node0Dir = Paths.get("hedera-node/test-clients") @@ -136,11 +123,11 @@ public static void main(String[] args) { .normalize(); final var validator = new StateChangesValidator( Bytes.fromHex( - "65374e72c2572aaaca17fe3a0e879841c0f5ae919348fc18231f8167bd28e326438c6f93a07a45eda7888b69e9812c4d"), + "912d5cf1478f1585f0d23ff8c7ecb05860b8a6c8c1f1d1ffe91d0fa45b642a98d54487d41f5966721a613ca646b28652"), node0Dir.resolve("output/swirlds.log"), node0Dir.resolve("config.txt"), node0Dir.resolve("data/config/application.properties"), - Bytes.fromHex("03")); + node0Dir.resolve("data/config")); final var blocks = BlockStreamAccess.BLOCK_STREAM_ACCESS.readBlocks(node0Dir.resolve("data/blockStreams/block-0.0.3")); validator.validateBlocks(blocks); @@ -189,8 +176,7 @@ public static StateChangesValidator newValidatorFor(@NonNull final HapiSpec spec node0.getExternalPath(SWIRLDS_LOG), genesisConfigTxt, node0.getExternalPath(APPLICATION_PROPERTIES), - requireNonNull(new BytesConverter() - .convert(spec.startupProperties().get("ledger.id")))); + node0.getExternalPath(DATA_CONFIG_DIR)); } catch (IOException e) { throw new UncheckedIOException(e); } @@ -201,50 +187,32 @@ public StateChangesValidator( @NonNull final Path pathToNode0SwirldsLog, @NonNull final Path pathToAddressBook, @NonNull final Path pathToOverrideProperties, - @NonNull final Bytes ledgerId) { + @NonNull final Path pathToUpgradeSysFilesLoc) { this.expectedRootHash = requireNonNull(expectedRootHash); this.pathToNode0SwirldsLog = requireNonNull(pathToNode0SwirldsLog); - // Ensure the bootstrap config sees our blockStream.streamMode=BOTH override - // and registers the BlockStreamService schemas System.setProperty( "hedera.app.properties.path", pathToOverrideProperties.toAbsolutePath().toString()); + System.setProperty( + "networkAdmin.upgradeSysFilesLoc", + pathToUpgradeSysFilesLoc.toAbsolutePath().toString()); + unarchiveGenesisNetworkJson(pathToUpgradeSysFilesLoc); final var bootstrapConfig = new BootstrapConfigProviderImpl().getConfiguration(); final var versionConfig = bootstrapConfig.getConfigData(VersionConfig.class); final var servicesVersion = versionConfig.servicesVersion(); final var addressBook = loadAddressBookWithDeterministicCerts(pathToAddressBook); - final var configVersion = - bootstrapConfig.getConfigData(HederaConfig.class).configVersion(); - final var currentVersion = new ServicesSoftwareVersion(servicesVersion, configVersion); final var metrics = new NoOpMetrics(); - final var hedera = new Hedera( - ConstructableRegistry.getInstance(), - ServicesRegistryImpl::new, - new OrderedServiceMigrator(), - InstantSource.system(), - appContext -> new TssBaseServiceImpl( - appContext, - ForkJoinPool.commonPool(), - ForkJoinPool.commonPool(), - new TssLibraryImpl(appContext), - ForkJoinPool.commonPool(), - metrics), - DiskStartupNetworks::new, - NodeId.of(0L)); + final var hedera = ServicesMain.newHedera(NodeId.of(0L), metrics); this.state = (PlatformMerkleStateRoot) hedera.newMerkleStateRoot(); - final Configuration platformConfig = ConfigurationBuilder.create() - .withConfigDataType(MetricsConfig.class) - .withConfigDataType(TransactionConfig.class) - .withConfigDataType(CryptoConfig.class) - .withConfigDataType(BasicConfig.class) - .withConfigDataType(VirtualMapConfig.class) - .withConfigDataType(MerkleDbConfig.class) - .withConfigDataType(TemporaryFileConfig.class) - .withConfigDataType(StateCommonConfig.class) - .build(); - hedera.initializeStatesApi(state, metrics, InitTrigger.GENESIS, addressBook, platformConfig); - initGenesisPlatformState(platformConfig, this.state.getWritablePlatformState(), addressBook, currentVersion); + final var platformConfig = ServicesMain.buildPlatformConfig(); + hedera.initializeStatesApi( + state, + metrics, + InitTrigger.GENESIS, + DiskStartupNetworks.fromLegacyAddressBook(addressBook), + platformConfig, + addressBook); final var stateToBeCopied = state; state = state.copy(); // get the state hash before applying the state changes from current block @@ -259,9 +227,11 @@ public void validateBlocks(@NonNull final List blocks) { var previousBlockHash = BlockStreamManager.ZERO_BLOCK_HASH; var startOfStateHash = requireNonNull(genesisStateHash).getBytes(); - for (int i = 0; i < blocks.size(); i++) { + final int n = blocks.size(); + for (int i = 0; i < n; i++) { final var block = blocks.get(i); - if (i != 0) { + final var shouldVerifyProof = i == 0 || i == n - 1 || RANDOM.nextDouble() < PROOF_VERIFICATION_PROB; + if (i != 0 && shouldVerifyProof) { final var stateToBeCopied = state; this.state = stateToBeCopied.copy(); startOfStateHash = CRYPTO.digestTreeSync(stateToBeCopied).getBytes(); @@ -270,7 +240,9 @@ public void validateBlocks(@NonNull final List blocks) { final StreamingTreeHasher outputTreeHasher = new NaiveStreamingTreeHasher(); for (final var item : block.items()) { servicesWritten.clear(); - hashInputOutputTree(item, inputTreeHasher, outputTreeHasher); + if (shouldVerifyProof) { + hashInputOutputTree(item, inputTreeHasher, outputTreeHasher); + } if (item.hasStateChanges()) { applyStateChanges(item.stateChangesOrThrow()); } @@ -284,10 +256,20 @@ public void validateBlocks(@NonNull final List blocks) { blockProof.previousBlockRootHash(), "Previous block hash mismatch for block " + blockProof.block()); - final var expectedBlockHash = - computeBlockHash(startOfStateHash, previousBlockHash, inputTreeHasher, outputTreeHasher); - validateBlockProof(blockProof, expectedBlockHash); - previousBlockHash = expectedBlockHash; + if (shouldVerifyProof) { + final var expectedBlockHash = + computeBlockHash(startOfStateHash, previousBlockHash, inputTreeHasher, outputTreeHasher); + validateBlockProof(blockProof, expectedBlockHash); + previousBlockHash = expectedBlockHash; + } else { + previousBlockHash = i < n - 1 + ? blocks.get(i + 1) + .items() + .getFirst() + .blockHeaderOrThrow() + .previousBlockHash() + : Bytes.EMPTY; + } } logger.info("Summary of changes by service:\n{}", stateChangesSummary); CRYPTO.digestTreeSync(state); @@ -411,6 +393,28 @@ private void applyStateChanges(@NonNull final StateChanges stateChanges) { } } + /** + * If the given path does not contain the genesis network JSON, recovers it from the archive directory. + * @param path the path to the network directory + * @throws IllegalStateException if the genesis network JSON cannot be found + * @throws UncheckedIOException if an I/O error occurs + */ + private void unarchiveGenesisNetworkJson(@NonNull final Path path) { + final var desiredPath = path.resolve(DiskStartupNetworks.GENESIS_NETWORK_JSON); + if (!desiredPath.toFile().exists()) { + final var archivedPath = + path.resolve(DiskStartupNetworks.ARCHIVE).resolve(DiskStartupNetworks.GENESIS_NETWORK_JSON); + if (!archivedPath.toFile().exists()) { + throw new IllegalStateException("No archived genesis network JSON found at " + archivedPath); + } + try { + Files.move(archivedPath, desiredPath); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } + private record ServiceChangesSummary( Map singletonPuts, Map mapUpdates, @@ -591,7 +595,6 @@ private static Object singletonPutFor(@NonNull final SingletonUpdateChange singl case BLOCK_STREAM_INFO_VALUE -> singletonUpdateChange.blockStreamInfoValueOrThrow(); case PLATFORM_STATE_VALUE -> singletonUpdateChange.platformStateValueOrThrow(); case ROSTER_STATE_VALUE -> singletonUpdateChange.rosterStateValueOrThrow(); - case TSS_STATUS_STATE_VALUE -> singletonUpdateChange.tssStatusStateValueOrThrow(); }; } @@ -649,7 +652,7 @@ private static Object mapValueFor(@NonNull final MapChangeValue mapChangeValue) case ROSTER_VALUE -> mapChangeValue.rosterValueOrThrow(); case SCHEDULED_COUNTS_VALUE -> mapChangeValue.scheduledCountsValueOrThrow(); case THROTTLE_USAGE_SNAPSHOTS_VALUE -> mapChangeValue.throttleUsageSnapshotsValue(); - case TSS_ENCRYPTION_KEY_VALUE -> mapChangeValue.tssEncryptionKeyValueOrThrow(); + case TSS_ENCRYPTION_KEYS_VALUE -> mapChangeValue.tssEncryptionKeysValue(); case TSS_MESSAGE_VALUE -> mapChangeValue.tssMessageValueOrThrow(); case TSS_VOTE_VALUE -> mapChangeValue.tssVoteValueOrThrow(); }; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/HapiSpec.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/HapiSpec.java index f32daa2a258a..d6d7b09df00f 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/HapiSpec.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/HapiSpec.java @@ -16,6 +16,7 @@ package com.hedera.services.bdd.spec; +import static com.hedera.node.app.roster.schemas.V0540RosterSchema.ROSTER_STATES_KEY; import static com.hedera.node.app.service.addressbook.impl.schemas.V053AddressBookSchema.NODES_KEY; import static com.hedera.node.app.service.token.impl.schemas.V0490TokenSchema.ACCOUNTS_KEY; import static com.hedera.node.app.service.token.impl.schemas.V0490TokenSchema.TOKENS_KEY; @@ -47,7 +48,6 @@ import static com.hedera.services.bdd.suites.HapiSuite.DEFAULT_CONTRACT_SENDER; import static com.hedera.services.bdd.suites.HapiSuite.ETH_SUFFIX; import static com.hedera.services.bdd.suites.HapiSuite.SECP_256K1_SOURCE_KEY; -import static com.swirlds.platform.state.service.schemas.V0540RosterSchema.ROSTER_STATES_KEY; import static java.util.Collections.emptyList; import static java.util.Objects.requireNonNull; import static java.util.concurrent.CompletableFuture.allOf; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/tss/RekeyScenarioOp.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/tss/RekeyScenarioOp.java index 9fdbcc7ccf2b..c81e2633a9f2 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/tss/RekeyScenarioOp.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/tss/RekeyScenarioOp.java @@ -54,6 +54,7 @@ import com.hedera.hapi.node.base.Transaction; import com.hedera.hapi.node.state.common.EntityNumber; import com.hedera.hapi.node.state.roster.Roster; +import com.hedera.hapi.node.state.roster.RosterEntry; import com.hedera.hapi.node.state.tss.TssMessageMapKey; import com.hedera.hapi.node.state.tss.TssVoteMapKey; import com.hedera.hapi.node.transaction.SignedTransaction; @@ -415,13 +416,19 @@ private Stream simulateOtherNodeTssMessages() { private SpecOperation extractRosterMetadata() { return doingContextual(spec -> { final var state = spec.embeddedStateOrThrow(); - final var hedera = spec.repeatableEmbeddedHederaOrThrow(); - final var roundNo = hedera.lastRoundNo(); - + final var embeddedHedera = spec.repeatableEmbeddedHederaOrThrow(); + final var roundNo = embeddedHedera.lastRoundNo(); + final var hedera = embeddedHedera.hedera(); final var writableStates = state.getWritableStates(RosterService.NAME); final var rosterStore = new WritableRosterStore(writableStates); - final var activeEntries = - new ArrayList<>(rosterStore.getActiveRoster().rosterEntries()); + + if (!hedera.isRosterLifecycleEnabled() && rosterStore.getActiveRoster() == null) { + rosterStore.putActiveRoster(embeddedHedera.roster(), 0); + ((CommittableWritableStates) writableStates).commit(); + } + + final List activeEntries = new ArrayList<>( + requireNonNull(rosterStore.getActiveRoster()).rosterEntries()); activeEntries.set( activeEntries.size() - 1, activeEntries.getLast().copyBuilder().weight(0).build()); @@ -439,7 +446,8 @@ private SpecOperation extractRosterMetadata() { .forEach((nodeId, numShares) -> activeShares.put(nodeId, numShares.intValue())); expectedMessages = activeShares.get(0L); // Prepare the FakeTssLibrary to decrypt the private shares of the embedded node - hedera.tssBaseService() + embeddedHedera + .tssBaseService() .fakeTssLibrary() .setupDecryption( directory -> {}, diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip423/LongTermScheduleUtils.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip423/LongTermScheduleUtils.java index 618287392b04..a5d8f3e37eeb 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip423/LongTermScheduleUtils.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip423/LongTermScheduleUtils.java @@ -151,6 +151,6 @@ public static SpecOperation[] triggerSchedule(String schedule, long waitForSecon } public static SpecOperation[] triggerSchedule(String schedule) { - return triggerSchedule(schedule, 5); + return triggerSchedule(schedule, 6); } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip423/ScheduleLongTermCreateTests.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip423/ScheduleLongTermCreateTests.java index 8be48714ad16..d2c80a5bb9ef 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip423/ScheduleLongTermCreateTests.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip423/ScheduleLongTermCreateTests.java @@ -60,15 +60,4 @@ final Stream scheduleCreateDefaultsToMaxValueFromConfig() { .via("createTxn"), getScheduleInfo("one").hasRelativeExpiry("createTxn", MAX_SCHEDULE_EXPIRY_TIME)); } - - @HapiTest - final Stream scheduleCreateMinimumTime() { - return hapiTest( - cryptoCreate(RECEIVER).balance(0L), - scheduleCreate("one", cryptoTransfer(tinyBarsFromTo(DEFAULT_PAYER, RECEIVER, 1L))) - .waitForExpiry(false) - .expiringIn(1) - .via("createTxn"), - getScheduleInfo("one").isExecuted().hasRelativeExpiry("createTxn", 1)); - } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip869/NodeCreateTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip869/NodeCreateTest.java index 7b5918a50612..42a7276baf35 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip869/NodeCreateTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip869/NodeCreateTest.java @@ -63,13 +63,11 @@ import com.hedera.services.bdd.junit.HapiTestLifecycle; import com.hedera.services.bdd.junit.LeakyEmbeddedHapiTest; import com.hedera.services.bdd.junit.LeakyHapiTest; -import com.hedera.services.bdd.junit.support.TestLifecycle; import com.hedera.services.bdd.spec.keys.KeyShape; import com.hederahashgraph.api.proto.java.AccountID; import com.hederahashgraph.api.proto.java.ServiceEndpoint; import com.swirlds.platform.system.address.Address; import com.swirlds.platform.test.fixtures.addressbook.RandomAddressBookBuilder; -import edu.umd.cs.findbugs.annotations.NonNull; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; import java.util.Arrays; @@ -100,7 +98,7 @@ public class NodeCreateTest { private static List gossipCertificates; @BeforeAll - static void beforeAll(@NonNull final TestLifecycle testLifecycle) { + static void beforeAll() { gossipCertificates = generateX509Certificates(2); } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/integration/ConcurrentIntegrationTests.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/integration/ConcurrentIntegrationTests.java index 2c5e450ab7aa..aeac2ed037e7 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/integration/ConcurrentIntegrationTests.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/integration/ConcurrentIntegrationTests.java @@ -19,6 +19,8 @@ import static com.hedera.hapi.node.base.HederaFunctionality.NODE_STAKE_UPDATE; 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.roster.schemas.V0540RosterSchema.ROSTER_KEY; +import static com.hedera.node.app.roster.schemas.V0540RosterSchema.ROSTER_STATES_KEY; import static com.hedera.services.bdd.junit.EmbeddedReason.MANIPULATES_EVENT_VERSION; import static com.hedera.services.bdd.junit.SharedNetworkLauncherSessionListener.CLASSIC_HAPI_TEST_NETWORK_SIZE; import static com.hedera.services.bdd.junit.TestTags.INTEGRATION; @@ -64,8 +66,6 @@ import static com.hedera.services.bdd.suites.hip869.NodeCreateTest.generateX509Certificates; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.RECORD_NOT_FOUND; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_NOT_ASSOCIATED_TO_ACCOUNT; -import static com.swirlds.platform.state.service.schemas.V0540RosterSchema.ROSTER_KEY; -import static com.swirlds.platform.state.service.schemas.V0540RosterSchema.ROSTER_STATES_KEY; import static org.junit.jupiter.api.Assertions.assertEquals; import com.hedera.hapi.block.stream.BlockItem; @@ -76,7 +76,7 @@ import com.hedera.hapi.node.state.roster.Roster; import com.hedera.hapi.node.state.roster.RosterState; import com.hedera.node.app.roster.RosterService; -import com.hedera.services.bdd.junit.BootstrapOverride; +import com.hedera.services.bdd.junit.ConfigOverride; import com.hedera.services.bdd.junit.EmbeddedHapiTest; import com.hedera.services.bdd.junit.GenesisHapiTest; import com.hedera.services.bdd.junit.HapiTest; @@ -209,7 +209,7 @@ final Stream failInvalidDuringDispatchRechargesFees() { Optional.ofNullable(amount == ONE_HUNDRED_HBARS ? "Fee was not recharged" : null))); } - @GenesisHapiTest(bootstrapOverrides = {@BootstrapOverride(key = "addressBook.useRosterLifecycle", value = "true")}) + @GenesisHapiTest(bootstrapOverrides = {@ConfigOverride(key = "addressBook.useRosterLifecycle", value = "true")}) @DisplayName("freeze upgrade with roster lifecycle sets candidate roster") final Stream freezeUpgradeWithRosterLifecycleSetsCandidateRoster() throws CertificateEncodingException { diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/integration/RepeatableHip423Tests.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/integration/RepeatableHip423Tests.java index 3f8f38e43d42..08aa8d8bf0df 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/integration/RepeatableHip423Tests.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/integration/RepeatableHip423Tests.java @@ -203,16 +203,13 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DynamicTest; -import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.TestMethodOrder; @Order(3) @Tag(INTEGRATION) @HapiTestLifecycle @TargetEmbeddedMode(REPEATABLE) -@TestMethodOrder(OrderAnnotation.class) public class RepeatableHip423Tests { private static final long ONE_MINUTE = 60; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/integration/RepeatableRosterLifecycleRestartTests.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/integration/RepeatableRosterLifecycleRestartTests.java new file mode 100644 index 000000000000..e92ee121038c --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/integration/RepeatableRosterLifecycleRestartTests.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.services.bdd.suites.integration; + +import static com.hedera.services.bdd.junit.TestTags.INTEGRATION; +import static com.hedera.services.bdd.junit.hedera.embedded.EmbeddedMode.REPEATABLE; +import static com.hedera.services.bdd.junit.restart.RestartType.GENESIS; +import static com.hedera.services.bdd.junit.restart.StartupAssets.ROSTER_ONLY; +import static com.hedera.services.bdd.spec.HapiSpec.hapiTest; + +import com.hedera.services.bdd.junit.ConfigOverride; +import com.hedera.services.bdd.junit.TargetEmbeddedMode; +import com.hedera.services.bdd.junit.restart.RestartHapiTest; +import java.util.stream.Stream; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Tag; + +@Order(3) +@Tag(INTEGRATION) +@TargetEmbeddedMode(REPEATABLE) +public class RepeatableRosterLifecycleRestartTests { + @RestartHapiTest( + restartType = GENESIS, + restartOverrides = {@ConfigOverride(key = "addressBook.useRosterLifecycle", value = "true")}, + restartAssets = ROSTER_ONLY) + Stream genesisMigrationReflectsInitialRoster() { + return hapiTest(); + } +} 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 4567e60901aa..3ccf526dd253 100644 --- a/hedera-node/test-clients/src/main/java/module-info.java +++ b/hedera-node/test-clients/src/main/java/module-info.java @@ -62,6 +62,7 @@ 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.restart; requires com.hedera.cryptography.bls; requires com.hedera.cryptography.pairings.api; @@ -84,7 +85,6 @@ requires com.swirlds.base; requires com.swirlds.common; requires com.swirlds.config.api; - requires com.swirlds.config.extensions; requires com.swirlds.merkledb; requires com.swirlds.metrics.api; requires com.swirlds.platform.core.test.fixtures; diff --git a/hedera-node/test-clients/src/main/resources/hapi-test-gossip-certs.json b/hedera-node/test-clients/src/main/resources/hapi-test-gossip-certs.json new file mode 100644 index 000000000000..15a3691cd251 --- /dev/null +++ b/hedera-node/test-clients/src/main/resources/hapi-test-gossip-certs.json @@ -0,0 +1,7 @@ +{ + "0" : "MIIDpzCCAg+gAwIBAgIJAK05TS8KZeb1MA0GCSqGSIb3DQEBDAUAMBIxEDAOBgNVBAMTB3Mtbm9kZTEwIBcNMDAwMTAxMDAwMDAwWhgPMjEwMDAxMDEwMDAwMDBaMBIxEDAOBgNVBAMTB3Mtbm9kZTEwggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQDBoP9dI3K1PRLRK7h90D9eNCfgzuHTyJi70yDEs90XJXlE6jmgf1NE2av83VAhQHLxu8Ehc/55M9Ayx9IQc0zJLSS+IrRM9QwqoG8ZvNdRgNw+je3V/8rAK/mHId+cPnnyDplCyskyi5kWCv6kTULIewFH8/KVZwhe0/hB2+N6ujWixURrxjjGLHA6b2gPoGAb/nxiVOn+L0cWcOzcyiYShxagj0FBWV7AxKx65Ynzfe7eF0gOzBUA+IM10OM5KXJejk53Xz5KpEyGe8htO/bXFlpLdm3UzrYiIhY0oKPYKECAC1s+VAZA6i+MV0nDpqDgxHRRXD8O2arauPhEI6iVT9f05AtzElrs7U95HbpQUuP1sxkaQw+bLdMOQHHMVCgMgw2g0eDdVDAMJD7wjZ+Bs6kDc/EJELb0l1uy2GEnOZMiHkK4K1r4IyZ/ed6QpyIRKfBCNyT5IIpMoVpzRYxVXgjgFdudd8iErKyvSXHThU6nu92c+vSd+FLBFHPpb6ECAwEAATANBgkqhkiG9w0BAQwFAAOCAYEAdga5NYtV48uDCd4vIsmpGWpKuUHtDVDlCvzHc2ij8DxAR6OFp+hIRNEBXkzg1KS5qP8Wba5ptmGoV4f89HemP+AL3Azde+HjpYRtffdfTdQwmMbw7xJg2lKkEo11gDo5+zPZnVbfb3FsZ+IXKji0QshQBfg+ddTkFG3TJG1ttq3ZDw94RxFQivVnkj1p+Ogel/DuBNRWQobFVe5VrmJqbuwwN8AdrPae1dMrkZatF91On5+cpVLGfk96fYUhDohDt6KKQ6DdhvFk5rhd0vsHGMQq2gAW2+Or6ZVsKkHKx8CPINpJVKAdpE0tItI+loMO02jf9oRI/8cThWP1vNAeWnr0D6m275EZf/4qem/DdJ0FJIVou3P7tsq7eSdueDnj5RmcbW/vOBtvlXpD3SqsVRn6sltZ0sk24p+6ZMzopevCZEMf/nL3OzGvSadisXb39H9DgwkNLlefju1QLgHWf0TGfeNHluDgVDhU8+/1/KUGtr2SnZ5EVO1l59FWHALj", + "1" : "MIIDpjCCAg6gAwIBAgIIHWg7e2Q/smQwDQYJKoZIhvcNAQEMBQAwEjEQMA4GA1UEAxMHcy1ub2RlMjAgFw0wMDAxMDEwMDAwMDBaGA8yMTAwMDEwMTAwMDAwMFowEjEQMA4GA1UEAxMHcy1ub2RlMjCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAKr5WsBepS3+y/0/yfBjzMWje7zianEz7sszrNWV3cGu2KUlR7v2+9wp/EtX1+BdcGlTTojgFs5nEBN4lM76Cp6JjFH461yN8GSkIkpe8GZnb1w4KEjZj5UYMbq+qOUI6QmwmgLeO8RHAsS6lCP1AyGFalb2ZVJ09DcYDxCRXeFj4BqvNbtD5r5DTCtpVT4ax3eb3pzNSGsjQUG9zhyp/WcsAmwmzKdMl72tk6qF8tlAWXyzwiCujWHS0Kln0C5pyEjeFNsG299toC4pgT8juxijgseTeIFRnNHmGSeSmXpAkEELlwLKR8HOnqeiS5UXNqdbxNemx/EpJSc5rTB6kzLX24dIuRsgyIIFWx73goOzmaHUolN4xmenifoMYlSNNM07WrsvmjRC5OLc/uGhdWqhZGBCH6AJB8Cmw84QLXVdHE6LiueP1oMd7g++N4X880wJkuh0ebfV3i7etUIn0jLlM50AkRucG9kwZDJ/M4LY7FT2F85R1/o2FaB/537ARQIDAQABMA0GCSqGSIb3DQEBDAUAA4IBgQB5lTkqYw0hEW+BJTFsQ8jEHfIDNRJ0kNbVuibfP+u7kzlJy15lCEi+Qw6E3d8hA1QBX3xJMxNBlrtYPrdG26hh/tOwo5Np/OfxQC5jo0Q7n7hu7aLxZRUB/q7AfdDbOun4Za6rJhT3+EsFocyARWp8bYSk3YILBMkP+2VYDRkgQidzKgKtO5yv21Y9sEgziSprc+dQb/tqn5aQZLWavFwCLwnB3t4r4qwLHkkH00Jw51uOvLeM49/t333V5Caa7wmWzMcE+KSWW0QWFRxeJrodSyjPdmDi4D8lKN5WJHSAU5L2yWIODUyWD/cvsAapTv7xXk9ja/Ssb9DpMQnM1xh0hYaESajNeL1QbGuZgPxAwrw981h7kprR2P2iMGRVGA6u4ezxmhW3s7D+yJ3+Yxs/x2J/sw65Z16mRYXRWYWHQmhgaVQjIviiAkVB6CWZo1kHl/eYaVedQzKlrTpbr3JtmwGwhYEOnrkzsC63h8/AG9gRtIAIGWGqTPWbn2pEm8M=", + "2" : "MIIDpzCCAg+gAwIBAgIJAJg3GRFp5bT9MA0GCSqGSIb3DQEBDAUAMBIxEDAOBgNVBAMTB3Mtbm9kZTMwIBcNMDAwMTAxMDAwMDAwWhgPMjEwMDAxMDEwMDAwMDBaMBIxEDAOBgNVBAMTB3Mtbm9kZTMwggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCl5ut2dCleDmgEneRYpAKa9Pe2qnXzgF+BEIuTfizG2OcPQi/ltv+6HxSrJXtuWNaiX/G4iP7iBzWj2ysaAYwfYj0ezTSMLRqM9hXzVgLtW0LJEF6a8vUXPsJt4GEJkUKiYCCO1MP1NLd3y/3SVJrFhwJSPqKYm2pQNg84WfPDWSkzSneOIO4Z0uWDXgs+vzSNyChWOxVieFQhLjcELtyj6narmLox+Jdo/SxUzPuktuFB3ebNgUqWPkjljgZpl00BTmbRIVHgHfDVulo2PBpXd0VplIDgdPr5zMKdTrKCuDKey8Mft72RkPKMe9LZVZ/21+rXVEh+olvvUCySsP2RkWPUJJD90c8wKo01rZsjAOXscJKQcBYlam5XXO4ZBRYzEdxuivbkPwsOoQ83swCR3alPvwfbg11Va+zXE6sRbUM9LqkYo/M3Hwg8tSIXu8oah6csputanz867dzWwyVJEPzmiXZ6ncVDQO31QlB7RndWCqKTjOQpnpblUMsrE9MCAwEAATANBgkqhkiG9w0BAQwFAAOCAYEAnUA8+kz7L+eSOm/iVvUNYF10PKO2nZtxWWL7R1vwK/2Up765PwqxKb0eSEM4bjgvZq1GuGXs9X/Y7dos42yntXvgeUY+/2JzCnw4J5tzxytZ+IKX6DR67NjDzDzVZQfptjLQrb8E7yzml0uxsqrhNPWl57Bmfe66Kg2lD11jImeeEhExlRggFukoiUWVwRNU21Q1jMUWrg2ZwfP+6fFTgRt0WR+X5zkyYPbvI6/yv7reYGjPDuZTOFhbwG8LUTQxdttDswPjnQ606kMyninL+aNelSdV/UIII7lpr/dTvgQAnrlBaGXvdy6brh3wWEwia0FZFZcKEs6M+jZ3MrFxvlTfUIdI3jRq12L10cCDi2VhORg4JmvlM+Tk6kJeSku30ZLAVo3S7GbTdvkuesOxz3UwnF7yfOA1KYOPvhv1oLxGV5z05glsn1OBKnXMdzsKFbAYYHj81bgBni2WLuIpv3oXlai2uc4y9m8LvWAQ+h/ivyog34Ai3Pvr5ZZOFgjy", + "3" : "MIIDpzCCAg+gAwIBAgIJAN7hww13zBZEMA0GCSqGSIb3DQEBDAUAMBIxEDAOBgNVBAMTB3Mtbm9kZTQwIBcNMDAwMTAxMDAwMDAwWhgPMjEwMDAxMDEwMDAwMDBaMBIxEDAOBgNVBAMTB3Mtbm9kZTQwggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQDK/bVyv0ZUeJZ4cIOImM+wmqtYjCw4jPAC549WQPPV1vG0lzSpgV+nRKqmWBexhLlKN3bsvrfNCUpKSq8meFyCtdppT1dhUOmEZcoNhLZzqxXb2HYYqRPv82tR+tbh+27WFsBOOqYrYTvr72ECD7qDOuw/Xob6KImaw/b/SIAPecMoYy25fkgYkJSETwd8HUpwssYH/JTLBF8eGjjTTMuu14ARQKeH8BXSs+jjV1+3IItXERS8ryUGDjqc5vC8ZW1kDVQbb91IDxRjqZbFyhuasocCqTAcZuiEgE8Wilwp2g1vbAUnHnvKNfiaEAHoEV6vF4lelaWhOnN2U5tnox/ns6PiDqIbOfs0pmXxjAK0vxc6oZM3TwdRtzo6cSb/AYfQdnmQzkra980kHN12r3f7PK2PzGBuVUPT7fLGA4S3vQDYO4rqcgTc/OLobtqLtdBusOFjZscfIfUW4GVWJUI1j+fwvHacxWLmyZwlQ5Q47UtrtjWpFru7CTn5S477lqMCAwEAATANBgkqhkiG9w0BAQwFAAOCAYEAdW6AWDhT0eOJw+0O6MYngmCgkXfFsgBC/B1plaE596hHo58FHxzCNiLFdvfRj37rxujvqsDAkADWUmOzLzLHYMXu302HzDqAMNY6FZJc32y4ZDsIQpaUOAuiNHAHwFXuPRInVpCqztfJMgw4RhOhcCTEsoIJsqoIN1t4M0pEVAv6x3nJwFKZqSNOZrQ7sOW32FjwWS3kHwRsCTtqdk5n2KxU6wr/fggV3QsSPRMYro8sUfwu93mqggtswwWqfeKlsz5WiaR9aqLnb8z1R6HLvA0bcoPWzjgn8RdP+9we4z06iZ5vdBuNpwBjrCKUELWISyAoekLGGxyS8pPqYiSBRNUoaPITSuUjcCBbJ9EFvm72QgCBesbwF71KPabTPbMPhLmf+uAi+zmeu8ZeVvT6DrX9OHSkIvIEQFry9BrqOT3ce6KBHSO1HpXIetj5Wcd3WHXtz9ulBL9ikWC8eh7/+we51ucmLvFzNKznElhT2Dp+czXUVNEUjp3u/66pyRA4", + "4" : "MIIDnjCCAgagAwIBAgIIcjC6xOVm1mcwDQYJKoZIhvcNAQEMBQAwDjEMMAoGA1UEAxMDcy0wMCAXDTAwMDEwMTAwMDAwMFoYDzIxMDAwMTAxMDAwMDAwWjAOMQwwCgYDVQQDEwNzLTAwggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCUXyWFpLmVcLYpB7IUaO4BpFNuIszXcyukusZAsbaiu5AQ6vuqSfxSbDTxUmn6IPcCP3fJUUmuGwITl/GDCalNfc6TF657ah3cBKWAyLUpnsmmTbCQ0QHUd2bjWEjm0XGb2cTLOYm8GlxCZFyFUEP8W/4orwZTlrUqi93ob5u3ZBY+orybEMN8zzkjLGrhc5d/lW7gFtHqpPC+RVFK9Nto34heYqZOYuWqJ/t/Aci5cO3cWsPNEdRSPdd5DGtpjlNVW86IA9xsMrd2dO8Civs9OF7I7K6UWWCqM+fKBHGr5Dsc5yy/mmUPo9e9afvSR3dDGDP7mInEyBlX5pOBSR/y1hn946uk0j51QWx2vjdhml2qZ6un8R92fCsBADVVkL6wFIZTsxQUgOEmkFOZmei+ikApNhh19gvJg5akl67Vnasp7xQOzZz1JweP7u18GFOcvLCXYPSXX9PSjPvZ76VGq0XfifzRYvKSBXwkz2poFFA8UllqAECdEfiZ19R3O+UCAwEAATANBgkqhkiG9w0BAQwFAAOCAYEAWrgsDVe06Y0hqL8i73JF0Q7aNJaTqaotWpTgH7eEBe3nqFTAJK3eLzhOHPSRiqk2iN1Vx8Y7iQ6XvIXJadIawPQgCxjPGsWN2EhJKq77agFtHiHnfo8RdTK09B/IF4OhNOiJ+vCvpRofXMVnagtQ0tcKrHETI9h6QHOAwK7ZhTI7bs5lDElFx8UMHBbZeSkyRCHleV+3VP+HT+1z9waHa+D07EwOoECG/BP1HXw4aZgZQPQlb+gGinZHv+Ou96+4DckCdcS32ihY/9XbbKHyn8KtrEsMD8Jk0NFRPT/IJToAWv4RrRDA4ie7AEnn1lOA7rXhiQojapldfmuDWRyXwYf+Igd0H79apJ7H7TchYXkUptajxLysobPNSY/kKuH/kqYCeXRvPVD3XcRbiUfFn3B3tnBU0hNsJzYIUA20iyT/iEjH9DFNm4bLqwyM9FD1LV/PJqnxoLm66Q38QYQxX16uXC7Rwmy96BI1b/rLIrXF9ZhPpVBqdBfGGRJdMemL" +} \ No newline at end of file diff --git a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/io/utility/FileUtilsTests.java b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/io/utility/FileUtilsTests.java index bfc4503317fe..283a4b1dee78 100644 --- a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/io/utility/FileUtilsTests.java +++ b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/io/utility/FileUtilsTests.java @@ -55,6 +55,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; +import java.util.stream.Stream; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -200,9 +201,14 @@ private static void assertDirectoryTreeEquality(final Path treeA, final Path tre assertEquals(isDirectory(treeA), isDirectory(treeB), "files should be the same type"); if (isDirectory(treeA)) { - final List childrenA = Files.list(treeA).toList(); - final List childrenB = Files.list(treeB).toList(); - + final List childrenA; + final List childrenB; + try (Stream list = Files.list(treeA)) { + childrenA = list.toList(); + } + try (Stream list = Files.list(treeB)) { + childrenB = list.toList(); + } assertEquals(childrenA.size(), childrenB.size(), "directories have a different number of files¬"); final Map mapA = new HashMap<>(); diff --git a/platform-sdk/swirlds-merkledb/src/test/java/com/swirlds/merkledb/VirtualMapSerializationTests.java b/platform-sdk/swirlds-merkledb/src/test/java/com/swirlds/merkledb/VirtualMapSerializationTests.java index ab28da6aaf87..e512190f00d0 100644 --- a/platform-sdk/swirlds-merkledb/src/test/java/com/swirlds/merkledb/VirtualMapSerializationTests.java +++ b/platform-sdk/swirlds-merkledb/src/test/java/com/swirlds/merkledb/VirtualMapSerializationTests.java @@ -62,6 +62,7 @@ import java.util.List; import java.util.Map; import java.util.Random; +import java.util.stream.Stream; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -272,10 +273,11 @@ private void testMapSerialization(final VirtualMap filesInDirectory = Files.list(savedStateDirectory).toList(); - assertNotNull(filesInDirectory, "saved state directory is not a valid directory"); - assertTrue(filesInDirectory.size() > 0, "there should be a non-zero number of files created"); - + try (final Stream filesInDirectory = Files.list(savedStateDirectory)) { + List list = filesInDirectory.toList(); + assertNotNull(list, "saved state directory is not a valid directory"); + assertTrue(list.size() > 0, "there should be a non-zero number of files created"); + } // Change default MerkleDb path, so data sources are restored into a different DB instance final Path restoredDbDirectory = LegacyTemporaryFileBuilder.buildTemporaryDirectory("merkledb-restored", CONFIGURATION); diff --git a/platform-sdk/swirlds-merkledb/src/timingSensitive/java/com/swirlds/merkledb/files/DataFileCollectionTest.java b/platform-sdk/swirlds-merkledb/src/timingSensitive/java/com/swirlds/merkledb/files/DataFileCollectionTest.java index 002d9f584342..0a3f7f000e1d 100644 --- a/platform-sdk/swirlds-merkledb/src/timingSensitive/java/com/swirlds/merkledb/files/DataFileCollectionTest.java +++ b/platform-sdk/swirlds-merkledb/src/timingSensitive/java/com/swirlds/merkledb/files/DataFileCollectionTest.java @@ -61,6 +61,7 @@ import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.IntStream; +import java.util.stream.Stream; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.core.Logger; import org.junit.jupiter.api.AfterEach; @@ -172,7 +173,11 @@ void createDataFileCollection(FilesTestType testType) throws Exception { count += 100; } // check 10 files were created - assertEquals(10, Files.list(tempFileDir.resolve(testType.name())).count(), "unexpected file count"); + int filesCount; + try (Stream list = Files.list(tempFileDir.resolve(testType.name()))) { + filesCount = (int) list.count(); + } + assertEquals(10, filesCount, "unexpected file count"); } @Order(3) @@ -223,13 +228,14 @@ void createDataFileCollectionWithLoadedDataCallback(final FilesTestType testType reinitializeDirectMemoryUsage(); // check that the 10 files were created previously (in the very first unit test) still are // readable - assertEquals( - 10, - Files.list(tempFileDir.resolve(testType.name())) - .filter(f -> f.toString().endsWith(".pbj")) - .filter(f -> !f.toString().contains("metadata")) - .count(), - "Temp file should not have changed since previous test in sequence"); + try (Stream list = Files.list(tempFileDir.resolve(testType.name()))) { + assertEquals( + 10, + list.filter(f -> f.toString().endsWith(".pbj")) + .filter(f -> !f.toString().contains("metadata")) + .count(), + "Temp file should not have changed since previous test in sequence"); + } // examine loadedDataCallbackImpl content's map sizes as well as checking the data assertEquals( 1000, @@ -436,13 +442,14 @@ public void forEach(final LongAction action) } }); // check we only have 1 file left - assertEquals( - 1, - Files.list(tempFileDir.resolve(testType.name())) - .filter(f -> f.toString().endsWith(".pbj")) - .filter(f -> !f.toString().contains("metadata")) - .count(), - "unexpected # of files #1"); + try (Stream list = Files.list(tempFileDir.resolve(testType.name()))) { + assertEquals( + 1, + list.filter(f -> f.toString().endsWith(".pbj")) + .filter(f -> !f.toString().contains("metadata")) + .count(), + "unexpected # of files #1"); + } // After merge is complete, there should be only 1 "fully written" file, and that it is // empty. List filesLeft = fileCollection.getAllCompletedFiles(); @@ -485,13 +492,14 @@ void changeSomeData(final FilesTestType testType) throws Exception { } fileCollection.endWriting(0, 1000).setFileCompleted(); // check we now have 2 files - assertEquals( - 2, - Files.list(tempFileDir.resolve(testType.name())) - .filter(f -> f.toString().endsWith(".pbj")) - .filter(f -> !f.toString().contains("metadata")) - .count(), - "unexpected # of files"); + try (Stream list = Files.list(tempFileDir.resolve(testType.name()))) { + assertEquals( + 2, + list.filter(f -> f.toString().endsWith(".pbj")) + .filter(f -> !f.toString().contains("metadata")) + .count(), + "unexpected # of files"); + } } @Order(201) @@ -592,13 +600,14 @@ public void forEach(final LongAction action) } }); // check we 7 files left, as we merged 5 out of 11 - assertEquals( - 1, - Files.list(tempFileDir.resolve(testType.name())) - .filter(f -> f.toString().endsWith(".pbj")) - .filter(f -> !f.toString().contains("metadata")) - .count(), - "unexpected # of files"); + try (Stream list = Files.list(tempFileDir.resolve(testType.name()))) { + assertEquals( + 1, + list.filter(f -> f.toString().endsWith(".pbj")) + .filter(f -> !f.toString().contains("metadata")) + .count(), + "unexpected # of files"); + } } private static DataFileCompactor createFileCompactor( @@ -643,12 +652,13 @@ void mergeWorksAfterOpen(final FilesTestType testType) throws Exception { // create 10x 100 item files populateDataFileCollection(testType, fileCollection, storedOffsets); // check 10 files were created and data is correct - assertEquals( - 10, - Files.list(dbDir) - .filter(file -> file.getFileName().toString().startsWith(storeName)) - .count(), - "expected 10 db files"); + try (Stream list = Files.list(dbDir)) { + assertEquals( + 10, + list.filter(file -> file.getFileName().toString().startsWith(storeName)) + .count(), + "expected 10 db files"); + } assertSame(10, fileCollection.getAllCompletedFiles().size(), "Should be 10 files"); checkData(fileCollectionMap.get(testType), storedOffsetsMap.get(testType), testType, 0, 1000, 10_000); // check all files are available for merge diff --git a/platform-sdk/swirlds-merkledb/src/timingSensitive/java/com/swirlds/merkledb/files/MemoryIndexDiskKeyValueStoreTest.java b/platform-sdk/swirlds-merkledb/src/timingSensitive/java/com/swirlds/merkledb/files/MemoryIndexDiskKeyValueStoreTest.java index 30df2423bdc0..548e1602dce3 100644 --- a/platform-sdk/swirlds-merkledb/src/timingSensitive/java/com/swirlds/merkledb/files/MemoryIndexDiskKeyValueStoreTest.java +++ b/platform-sdk/swirlds-merkledb/src/timingSensitive/java/com/swirlds/merkledb/files/MemoryIndexDiskKeyValueStoreTest.java @@ -38,6 +38,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.stream.IntStream; +import java.util.stream.Stream; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; @@ -212,11 +213,18 @@ int getMinNumberOfFilesToCompact() { writeBatch(testType, store, 1500, 2000, 3500, 1234); checkRange(testType, store, 0, 2000, 1234); // check number of files created - assertEquals(3, Files.list(tempDir).count(), "unexpected # of files #1"); + int filesCount; + try (Stream list = Files.list(tempDir)) { + filesCount = (int) list.count(); + } + assertEquals(3, filesCount, "unexpected # of files #1"); // compact all files dataFileCompactor.compact(); // check number of files after merge - assertEquals(1, Files.list(tempDir).count(), "unexpected # of files #2"); + try (Stream list = Files.list(tempDir)) { + filesCount = (int) list.count(); + } + assertEquals(1, filesCount, "unexpected # of files #2"); // check all data checkRange(testType, store, 0, 2000, 1234); // check metrics are reported @@ -265,23 +273,28 @@ int getMinNumberOfFilesToCompact() { checkRange(testType, store, 0, 2000, 8910); checkRange(testType, store, 2000, 48_000, 56_000); // check number of files created - assertEquals(2, Files.list(tempDir).count(), "unexpected # of files #3"); + try (Stream list = Files.list(tempDir)) { + filesCount = (int) list.count(); + } + assertEquals(2, filesCount, "unexpected # of files #3"); // create a snapshot final Path tempSnapshotDir = testDirectory.resolve("DataFileTestSnapshot"); store.snapshot(tempSnapshotDir); // check all files are in new dir - Files.list(tempDir).forEach(file -> { - assertTrue(Files.exists(tempSnapshotDir.resolve(file.getFileName())), "Expected file does not exist"); - try { - assertEquals( - Files.size(file), - Files.size(tempSnapshotDir.resolve(file.getFileName())), - "Unexpected value from Files.size()"); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); + try (Stream list = Files.list(tempDir)) { + list.forEach(file -> { + assertTrue(Files.exists(tempSnapshotDir.resolve(file.getFileName())), "Expected file does not exist"); + try { + assertEquals( + Files.size(file), + Files.size(tempSnapshotDir.resolve(file.getFileName())), + "Unexpected value from Files.size()"); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } // open snapshot and check data final LongListOffHeap snapshotIndex = new LongListOffHeap(); final MemoryIndexDiskKeyValueStore storeFromSnapshot = new MemoryIndexDiskKeyValueStore( diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/roster/RosterUtils.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/roster/RosterUtils.java index 7764a915fff9..26c157b7d23a 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/roster/RosterUtils.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/roster/RosterUtils.java @@ -20,6 +20,8 @@ import com.hedera.hapi.node.state.roster.Roster; import com.hedera.hapi.node.state.roster.RosterEntry; import com.hedera.hapi.node.state.roster.RoundRosterPair; +import com.hedera.node.internal.network.Network; +import com.hedera.node.internal.network.NodeMetadata; import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.common.crypto.CryptographyException; import com.swirlds.common.crypto.Hash; @@ -338,4 +340,15 @@ public static AddressBook buildAddressBook(@NonNull final Roster roster) { return addressBook; } + + /** + * Build a Roster object out of a given {@link Network} address book. + * @param network a network + * @return a Roster + */ + public static @NonNull Roster rosterFrom(@NonNull final Network network) { + return new Roster(network.nodeMetadata().stream() + .map(NodeMetadata::rosterEntryOrThrow) + .toList()); + } } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/GenesisStateBuilder.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/GenesisStateBuilder.java deleted file mode 100644 index da8f2a9b68b9..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/GenesisStateBuilder.java +++ /dev/null @@ -1,85 +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.swirlds.platform.state; - -import com.swirlds.config.api.Configuration; -import com.swirlds.platform.config.AddressBookConfig; -import com.swirlds.platform.config.BasicConfig; -import com.swirlds.platform.crypto.CryptoStatic; -import com.swirlds.platform.state.signed.ReservedSignedState; -import com.swirlds.platform.state.signed.SignedState; -import com.swirlds.platform.system.SoftwareVersion; -import com.swirlds.platform.system.address.AddressBook; -import edu.umd.cs.findbugs.annotations.NonNull; -import java.time.Instant; - -/** - * Responsible for building the genesis state. - */ -public final class GenesisStateBuilder { - - private GenesisStateBuilder() {} - - /** - * Initializes a genesis platform state. - * - */ - public static void initGenesisPlatformState( - final Configuration configuration, - final PlatformStateModifier platformState, - final AddressBook addressBook, - final SoftwareVersion appVersion) { - platformState.bulkUpdate(v -> { - v.setAddressBook(addressBook.copy()); - v.setCreationSoftwareVersion(appVersion); - v.setRound(0); - v.setLegacyRunningEventHash(null); - v.setConsensusTimestamp(Instant.ofEpochSecond(0L)); - - final BasicConfig basicConfig = configuration.getConfigData(BasicConfig.class); - - final long genesisFreezeTime = basicConfig.genesisFreezeTime(); - if (genesisFreezeTime > 0) { - v.setFreezeTime(Instant.ofEpochSecond(genesisFreezeTime)); - } - }); - } - - /** - * Build and initialize a genesis state. - * - * @param configuration the configuration for this node - * @param addressBook the current address book - * @param appVersion the software version of the app - * @param stateRoot the merkle root node of the state - * @return a reserved genesis signed state - */ - public static ReservedSignedState buildGenesisState( - @NonNull final Configuration configuration, - @NonNull final AddressBook addressBook, - @NonNull final SoftwareVersion appVersion, - @NonNull final MerkleRoot stateRoot) { - - if (!configuration.getConfigData(AddressBookConfig.class).useRosterLifecycle()) { - initGenesisPlatformState(configuration, stateRoot.getWritablePlatformState(), addressBook, appVersion); - } - - final SignedState signedState = new SignedState( - configuration, CryptoStatic::verifySignature, stateRoot, "genesis state", false, false, false); - return signedState.reserve("initial reservation on genesis state"); - } -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/PlatformMerkleStateRoot.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/PlatformMerkleStateRoot.java index d18a56265bd5..b6c9b1c7fc37 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/PlatformMerkleStateRoot.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/PlatformMerkleStateRoot.java @@ -231,7 +231,7 @@ private WritablePlatformStateStore writablePlatformStateStore() { private com.hedera.hapi.platform.state.PlatformState getPlatformState() { final var index = findNodeIndex(PlatformStateService.NAME, PLATFORM_STATE_KEY); return index == -1 - ? V0540PlatformStateSchema.GENESIS_PLATFORM_STATE + ? V0540PlatformStateSchema.UNINITIALIZED_PLATFORM_STATE : ((SingletonNode) getChild(index)).getValue(); } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/PlatformStateService.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/PlatformStateService.java index 55eef7dd48b9..5463506ee474 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/PlatformStateService.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/PlatformStateService.java @@ -20,12 +20,13 @@ import static java.util.Objects.requireNonNull; import com.hedera.hapi.node.base.SemanticVersion; -import com.hedera.hapi.node.state.roster.Roster; import com.hedera.hapi.platform.state.ConsensusSnapshot; import com.hedera.hapi.platform.state.PlatformState; +import com.swirlds.config.api.Configuration; import com.swirlds.platform.state.service.schemas.V0540PlatformStateSchema; -import com.swirlds.platform.state.service.schemas.V057PlatformStateSchema; +import com.swirlds.platform.state.service.schemas.V058RosterLifecycleTransitionSchema; import com.swirlds.platform.system.SoftwareVersion; +import com.swirlds.platform.system.address.AddressBook; import com.swirlds.state.lifecycle.Schema; import com.swirlds.state.lifecycle.SchemaRegistry; import com.swirlds.state.lifecycle.Service; @@ -36,7 +37,7 @@ import java.util.Collection; import java.util.List; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Supplier; +import java.util.function.Function; /** * A service that provides the schema for the platform state, used by {@link MerkleStateRoot} @@ -45,13 +46,26 @@ public enum PlatformStateService implements Service { PLATFORM_STATE_SERVICE; - private static final AtomicReference> ACTIVE_ROSTER = new AtomicReference<>(); - private static final AtomicReference> APP_VERSION = new AtomicReference<>(); + /** + * Temporary access to a function that computes an application version from config. + */ + private static final AtomicReference> APP_VERSION_FN = + new AtomicReference<>(); + /** + * Temporary access to the disk address book used in upgrade or network transplant + * scenarios before the roster lifecycle is enabled. + */ + @Deprecated + private static final AtomicReference DISK_ADDRESS_BOOK = new AtomicReference<>(); + /** + * The schemas to register with the {@link SchemaRegistry}. + */ private static final Collection SCHEMAS = List.of( - new V0540PlatformStateSchema(), - new V057PlatformStateSchema( - () -> requireNonNull(ACTIVE_ROSTER.get()).get(), - () -> requireNonNull(APP_VERSION.get()).get(), + new V0540PlatformStateSchema(DISK_ADDRESS_BOOK::get, config -> requireNonNull(APP_VERSION_FN.get()) + .apply(config)), + new V058RosterLifecycleTransitionSchema( + DISK_ADDRESS_BOOK::get, + config -> requireNonNull(APP_VERSION_FN.get()).apply(config), WritablePlatformStateStore::new)); public static final String NAME = "PlatformStateService"; @@ -69,26 +83,25 @@ public void registerSchemas(@NonNull final SchemaRegistry registry) { } /** - * Sets the active roster to the given roster. - * @param roster the roster to set as active + * Sets the application version to the given version. + * @param appVersionFn the version to set as the application version */ - public void setActiveRosterFn(@NonNull final Supplier roster) { - ACTIVE_ROSTER.set(requireNonNull(roster)); + public void setAppVersionFn(@NonNull final Function appVersionFn) { + APP_VERSION_FN.set(requireNonNull(appVersionFn)); } /** - * Clears the active roster. + * Sets the disk address book to the given address book. */ - public void clearActiveRosterFn() { - ACTIVE_ROSTER.set(null); + public void setDiskAddressBook(@NonNull final AddressBook addressBook) { + DISK_ADDRESS_BOOK.set(requireNonNull(addressBook)); } /** - * Sets the application version to the given version. - * @param appVersionFn the version to set as the application version + * Clears the disk address book. */ - public void setAppVersionFn(@NonNull final Supplier appVersionFn) { - APP_VERSION.set(requireNonNull(appVersionFn)); + public void clearDiskAddressBook() { + DISK_ADDRESS_BOOK.set(null); } /** @@ -96,7 +109,7 @@ public void setAppVersionFn(@NonNull final Supplier appVersionF * @param root the root to extract the creation version from * @return the creation version of the platform state, or null if the state is a genesis state */ - public SemanticVersion creationVersionOf(@NonNull final MerkleStateRoot root) { + public SemanticVersion creationVersionOf(@NonNull final MerkleStateRoot root) { requireNonNull(root); final var state = platformStateOf(root); return state == null ? null : state.creationSoftwareVersionOrThrow(); @@ -107,7 +120,7 @@ public SemanticVersion creationVersionOf(@NonNull final MerkleStateRoot root) { * @param root the root to extract the round number from * @return the round number of the platform state, or zero if the state is a genesis state */ - public long roundOf(@NonNull final MerkleStateRoot root) { + public long roundOf(@NonNull final MerkleStateRoot root) { requireNonNull(root); final var platformState = platformStateOf(root); return platformState == null @@ -118,7 +131,7 @@ public long roundOf(@NonNull final MerkleStateRoot root) { } @SuppressWarnings("unchecked") - public @Nullable PlatformState platformStateOf(@NonNull final MerkleStateRoot root) { + public @Nullable PlatformState platformStateOf(@NonNull final MerkleStateRoot root) { final var index = root.findNodeIndex(NAME, PLATFORM_STATE_KEY); if (index == -1) { return null; diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/WritableRosterStore.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/WritableRosterStore.java index 5a47b98fe1ea..dc1b52745605 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/WritableRosterStore.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/WritableRosterStore.java @@ -91,7 +91,7 @@ public void putCandidateRoster(@NonNull final Roster candidateRoster) { RosterUtils.hash(candidateRoster).getBytes(); // update the roster state/map - final RosterState previousRosterState = rosterStateOrThrow(); + final RosterState previousRosterState = rosterStateOrDefault(); final Bytes previousCandidateRosterHash = previousRosterState.candidateRosterHash(); final Builder newRosterStateBuilder = previousRosterState.copyBuilder().candidateRosterHash(incomingCandidateRosterHash); @@ -115,7 +115,7 @@ public void putActiveRoster(@NonNull final Roster roster, final long round) { RosterValidator.validate(roster); // update the roster state - final RosterState previousRosterState = rosterStateOrThrow(); + final RosterState previousRosterState = rosterStateOrDefault(); final List roundRosterPairs = new LinkedList<>(previousRosterState.roundRosterPairs()); if (!roundRosterPairs.isEmpty()) { final RoundRosterPair activeRosterPair = roundRosterPairs.getFirst(); @@ -152,13 +152,13 @@ public void putActiveRoster(@NonNull final Roster roster, final long round) { } /** - * Returns the roster state or throws an exception if the state is null. + * Returns the roster state; or the default roster state if the roster state is not yet set at genesis. * @return the roster state - * @throws NullPointerException if the roster state is null */ @NonNull - private RosterState rosterStateOrThrow() { - return requireNonNull(rosterState.get()); + private RosterState rosterStateOrDefault() { + RosterState state; + return (state = rosterState.get()) == null ? RosterState.DEFAULT : state; } /** @@ -168,7 +168,7 @@ private RosterState rosterStateOrThrow() { * @param rosterHash the hash of the roster */ private void removeRoster(@NonNull final Bytes rosterHash) { - final List activeRosterHistory = rosterStateOrThrow().roundRosterPairs(); + final List activeRosterHistory = rosterStateOrDefault().roundRosterPairs(); if (activeRosterHistory.stream() .noneMatch(rosterPair -> rosterPair.activeRosterHash().equals(rosterHash))) { this.rosterMap.remove(ProtoBytes.newBuilder().value(rosterHash).build()); diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/schemas/V0540PlatformStateSchema.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/schemas/V0540PlatformStateSchema.java index caf3f69b2079..2bfe21cfb961 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/schemas/V0540PlatformStateSchema.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/schemas/V0540PlatformStateSchema.java @@ -16,30 +16,68 @@ package com.swirlds.platform.state.service.schemas; +import static java.util.Objects.requireNonNull; + import com.hedera.hapi.node.base.SemanticVersion; import com.hedera.hapi.platform.state.ConsensusSnapshot; import com.hedera.hapi.platform.state.PlatformState; import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.swirlds.config.api.Configuration; +import com.swirlds.platform.config.AddressBookConfig; +import com.swirlds.platform.config.BasicConfig; +import com.swirlds.platform.state.PlatformStateModifier; +import com.swirlds.platform.state.service.WritablePlatformStateStore; +import com.swirlds.platform.system.SoftwareVersion; +import com.swirlds.platform.system.address.AddressBook; import com.swirlds.state.lifecycle.MigrationContext; import com.swirlds.state.lifecycle.Schema; import com.swirlds.state.lifecycle.StateDefinition; import edu.umd.cs.findbugs.annotations.NonNull; +import java.time.Instant; import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; /** - * This is a schema for the platform state that is represented by a singleton. The schema is responsible for - * registering {@link #GENESIS_PLATFORM_STATE} state if an instance of the state does not exist. + * Defines the {@link PlatformState} singleton and initializes it at genesis. */ public class V0540PlatformStateSchema extends Schema { - public static final PlatformState GENESIS_PLATFORM_STATE = new PlatformState( - SemanticVersion.DEFAULT, 0, ConsensusSnapshot.DEFAULT, null, null, Bytes.EMPTY, 0L, 0L, null, null, null); + private static final Supplier UNAVAILABLE_DISK_ADDRESS_BOOK = () -> { + throw new IllegalStateException("No disk address book available"); + }; + private static final Function UNAVAILABLE_VERSION_FN = config -> { + throw new IllegalStateException("No version information available"); + }; + public static final String PLATFORM_STATE_KEY = "PLATFORM_STATE"; + /** + * A platform state to be used as the non-null platform state under any circumstance a genesis state + * is encountered before initializing the States API. + */ + public static final PlatformState UNINITIALIZED_PLATFORM_STATE = new PlatformState( + SemanticVersion.DEFAULT, 0, ConsensusSnapshot.DEFAULT, null, null, Bytes.EMPTY, 0L, 0L, null, null, null); private static final SemanticVersion VERSION = SemanticVersion.newBuilder().major(0).minor(54).patch(0).build(); + private final Supplier addressBook; + private final Function versionFn; + public V0540PlatformStateSchema() { + this(UNAVAILABLE_DISK_ADDRESS_BOOK, UNAVAILABLE_VERSION_FN); + } + + public V0540PlatformStateSchema(@NonNull final Function versionFn) { + this(UNAVAILABLE_DISK_ADDRESS_BOOK, versionFn); + } + + public V0540PlatformStateSchema( + @NonNull final Supplier addressBook, + @NonNull final Function versionFn) { super(VERSION); + this.addressBook = requireNonNull(addressBook); + this.versionFn = requireNonNull(versionFn); } @NonNull @@ -50,9 +88,42 @@ public Set statesToCreate() { @Override public void migrate(@NonNull final MigrationContext ctx) { - final var platformState = ctx.newStates().getSingleton(PLATFORM_STATE_KEY); - if (platformState.get() == null) { - platformState.put(GENESIS_PLATFORM_STATE); + final var stateSingleton = ctx.newStates().getSingleton(PLATFORM_STATE_KEY); + if (ctx.isGenesis()) { + stateSingleton.put(UNINITIALIZED_PLATFORM_STATE); + final var genesisStateSpec = genesisStateSpec(ctx); + final var platformStateStore = new WritablePlatformStateStore(ctx.newStates()); + if (ctx.appConfig().getConfigData(AddressBookConfig.class).useRosterLifecycle()) { + // When using the roster lifecycle at genesis, platform code will never + // use the legacy previous/current AddressBook fields, so omit them + platformStateStore.bulkUpdate(genesisStateSpec); + } else { + final var book = addressBook.get(); + requireNonNull(book); + platformStateStore.bulkUpdate(genesisStateSpec.andThen(v -> { + v.setPreviousAddressBook(null); + v.setAddressBook(book.copy()); + })); + } + } else { + // (FUTURE) Delete this code path, it is only reached through the Browser entrypoint + if (stateSingleton.get() == null) { + stateSingleton.put(UNINITIALIZED_PLATFORM_STATE); + } } } + + private Consumer genesisStateSpec(@NonNull final MigrationContext ctx) { + return v -> { + v.setCreationSoftwareVersion(versionFn.apply(ctx.appConfig())); + v.setRound(0); + v.setLegacyRunningEventHash(null); + v.setConsensusTimestamp(Instant.EPOCH); + final var basicConfig = ctx.platformConfig().getConfigData(BasicConfig.class); + final long genesisFreezeTime = basicConfig.genesisFreezeTime(); + if (genesisFreezeTime > 0) { + v.setFreezeTime(Instant.ofEpochSecond(genesisFreezeTime)); + } + }; + } } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/schemas/V0540RosterSchema.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/schemas/V0540RosterBaseSchema.java similarity index 95% rename from platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/schemas/V0540RosterSchema.java rename to platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/schemas/V0540RosterBaseSchema.java index 32bba2830219..0fe1391c0406 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/schemas/V0540RosterSchema.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/schemas/V0540RosterBaseSchema.java @@ -31,11 +31,11 @@ /** * Roster Schema */ -public class V0540RosterSchema extends Schema { +public class V0540RosterBaseSchema extends Schema { public static final String ROSTER_KEY = "ROSTERS"; public static final String ROSTER_STATES_KEY = "ROSTER_STATE"; - private static final Logger log = LogManager.getLogger(V0540RosterSchema.class); + private static final Logger log = LogManager.getLogger(V0540RosterBaseSchema.class); /** * this can't be increased later so we pick some number large enough, 2^16. */ @@ -50,7 +50,7 @@ public class V0540RosterSchema extends Schema { /** * Create a new instance */ - public V0540RosterSchema() { + public V0540RosterBaseSchema() { super(VERSION); } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/schemas/V057PlatformStateSchema.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/schemas/V057PlatformStateSchema.java deleted file mode 100644 index 3ca476adb33f..000000000000 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/schemas/V057PlatformStateSchema.java +++ /dev/null @@ -1,109 +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.swirlds.platform.state.service.schemas; - -import static com.swirlds.state.lifecycle.HapiUtils.SEMANTIC_VERSION_COMPARATOR; -import static java.util.Objects.requireNonNull; - -import com.hedera.hapi.node.base.SemanticVersion; -import com.hedera.hapi.node.state.roster.Roster; -import com.hedera.node.internal.network.NodeMetadata; -import com.swirlds.platform.config.AddressBookConfig; -import com.swirlds.platform.config.BasicConfig; -import com.swirlds.platform.roster.RosterUtils; -import com.swirlds.platform.state.service.WritablePlatformStateStore; -import com.swirlds.platform.system.SoftwareVersion; -import com.swirlds.state.lifecycle.MigrationContext; -import com.swirlds.state.lifecycle.Schema; -import com.swirlds.state.spi.WritableStates; -import edu.umd.cs.findbugs.annotations.NonNull; -import java.time.Instant; -import java.util.function.Function; -import java.util.function.Supplier; - -/** - * Restart-only schema that ensures the platform state at startup - * reflects the active roster. - */ -public class V057PlatformStateSchema extends Schema { - private static final SemanticVersion VERSION = - SemanticVersion.newBuilder().major(0).minor(57).build(); - - /** - * A supplier for the active roster. - */ - private final Supplier activeRoster; - - private final Supplier appVersion; - private final Function platformStateStoreFactory; - - public V057PlatformStateSchema( - @NonNull final Supplier activeRoster, - @NonNull final Supplier appVersion, - @NonNull final Function platformStateStoreFactory) { - super(VERSION); - this.activeRoster = requireNonNull(activeRoster); - this.appVersion = requireNonNull(appVersion); - this.platformStateStoreFactory = requireNonNull(platformStateStoreFactory); - } - - @Override - public void restart(@NonNull final MigrationContext ctx) { - requireNonNull(ctx); - if (!ctx.configuration().getConfigData(AddressBookConfig.class).useRosterLifecycle()) { - return; - } - - final var platformStateStore = platformStateStoreFactory.apply(ctx.newStates()); - final var startupNetworks = ctx.startupNetworks(); - if (ctx.isGenesis()) { - final var genesisNetwork = startupNetworks.genesisNetworkOrThrow(); - final var roster = new Roster(genesisNetwork.nodeMetadata().stream() - .map(NodeMetadata::rosterEntryOrThrow) - .toList()); - final var addressBook = RosterUtils.buildAddressBook(roster); - platformStateStore.bulkUpdate(v -> { - v.setAddressBook(addressBook); - v.setPreviousAddressBook(null); - v.setCreationSoftwareVersion(appVersion.get()); - v.setRound(0); - v.setLegacyRunningEventHash(null); - v.setConsensusTimestamp(Instant.ofEpochSecond(0L)); - - final BasicConfig basicConfig = ctx.configuration().getConfigData(BasicConfig.class); - - final long genesisFreezeTime = basicConfig.genesisFreezeTime(); - if (genesisFreezeTime > 0) { - v.setFreezeTime(Instant.ofEpochSecond(genesisFreezeTime)); - } - }); - } else if (isUpgrade(ctx)) { - final var candidateAddressBook = RosterUtils.buildAddressBook(activeRoster.get()); - final var previousAddressBook = platformStateStore.getAddressBook(); - platformStateStore.bulkUpdate(v -> { - v.setAddressBook(candidateAddressBook.copy()); - v.setPreviousAddressBook(previousAddressBook == null ? null : previousAddressBook.copy()); - }); - } - } - - private boolean isUpgrade(@NonNull final MigrationContext ctx) { - final var currentVersion = appVersion.get().getPbjSemanticVersion(); - final var previousVersion = ctx.previousVersion(); - return SEMANTIC_VERSION_COMPARATOR.compare(currentVersion, (requireNonNull(previousVersion))) > 0; - } -} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/schemas/V058RosterLifecycleTransitionSchema.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/schemas/V058RosterLifecycleTransitionSchema.java new file mode 100644 index 000000000000..e2203d654fae --- /dev/null +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/schemas/V058RosterLifecycleTransitionSchema.java @@ -0,0 +1,106 @@ +/* + * 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.service.schemas; + +import static com.swirlds.platform.state.service.schemas.V0540PlatformStateSchema.PLATFORM_STATE_KEY; +import static com.swirlds.state.lifecycle.HapiUtils.SEMANTIC_VERSION_COMPARATOR; +import static java.util.Objects.requireNonNull; + +import com.hedera.hapi.node.base.SemanticVersion; +import com.hedera.hapi.platform.state.PlatformState; +import com.swirlds.config.api.Configuration; +import com.swirlds.platform.config.AddressBookConfig; +import com.swirlds.platform.state.service.WritablePlatformStateStore; +import com.swirlds.platform.system.SoftwareVersion; +import com.swirlds.platform.system.address.AddressBook; +import com.swirlds.state.lifecycle.MigrationContext; +import com.swirlds.state.lifecycle.Schema; +import com.swirlds.state.spi.WritableStates; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * A restart-only schema to ensure the platform state has its active and previous + * address books configured correctly when the roster lifecycle is disabled. + *

    + * (FUTURE) Delete at the same time as the {@link AddressBookConfig#useRosterLifecycle()} + * feature flag. + */ +@Deprecated +public class V058RosterLifecycleTransitionSchema extends Schema { + private static final SemanticVersion VERSION = + SemanticVersion.newBuilder().major(0).minor(58).build(); + + private final Supplier addressBook; + private final Function appVersionFn; + private final Function platformStateStoreFn; + + public V058RosterLifecycleTransitionSchema( + @NonNull final Supplier addressBook, + @NonNull final Function appVersionFn, + @NonNull final Function platformStateStoreFn) { + super(VERSION); + this.addressBook = requireNonNull(addressBook); + this.appVersionFn = requireNonNull(appVersionFn); + this.platformStateStoreFn = requireNonNull(platformStateStoreFn); + } + + @Override + public void migrate(@NonNull final MigrationContext ctx) { + requireNonNull(ctx); + if (ctx.appConfig().getConfigData(AddressBookConfig.class).useRosterLifecycle()) { + final var stateSingleton = ctx.newStates().getSingleton(PLATFORM_STATE_KEY); + final var state = requireNonNull(stateSingleton.get()); + // Null out the legacy address book fields + stateSingleton.put(state.copyBuilder() + .previousAddressBook((com.hedera.hapi.platform.state.AddressBook) null) + .addressBook(((com.hedera.hapi.platform.state.AddressBook) null)) + .build()); + } + } + + @Override + public void restart(@NonNull final MigrationContext ctx) { + requireNonNull(ctx); + final var addressBookConfig = ctx.appConfig().getConfigData(AddressBookConfig.class); + if (!addressBookConfig.useRosterLifecycle() && !ctx.isGenesis()) { + final boolean addressBookChanged = ctx.isUpgrade( + config -> new Semver(appVersionFn.apply(config).getPbjSemanticVersion()), Semver::new) + || addressBookConfig.forceUseOfConfigAddressBook(); + if (addressBookChanged) { + final var stateStore = platformStateStoreFn.apply(ctx.newStates()); + final var currentBook = stateStore.getAddressBook(); + stateStore.bulkUpdate(v -> { + v.setPreviousAddressBook(currentBook == null ? null : currentBook.copy()); + v.setAddressBook(requireNonNull(addressBook.get()).copy()); + }); + } + } + } + + /** + * A comparable wrapper around a {@link SemanticVersion} to allow for version comparison. + * @param version the version to wrap + */ + private record Semver(@NonNull SemanticVersion version) implements Comparable { + @Override + public int compareTo(@NonNull final Semver that) { + return SEMANTIC_VERSION_COMPARATOR.compare(this.version, that.version); + } + } +} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/signed/StartupStateUtils.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/signed/StartupStateUtils.java index 75b1a6624329..09e1e772815d 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/signed/StartupStateUtils.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/signed/StartupStateUtils.java @@ -19,7 +19,6 @@ import static com.swirlds.common.merkle.utility.MerkleUtils.rehashTree; import static com.swirlds.logging.legacy.LogMarker.EXCEPTION; import static com.swirlds.logging.legacy.LogMarker.STARTUP; -import static com.swirlds.platform.state.GenesisStateBuilder.buildGenesisState; import static com.swirlds.platform.state.signed.ReservedSignedState.createNullReservation; import static com.swirlds.platform.state.snapshot.SignedStateFileReader.readStateFile; import static java.util.Objects.requireNonNull; @@ -31,10 +30,13 @@ import com.swirlds.common.platform.NodeId; import com.swirlds.config.api.Configuration; import com.swirlds.logging.legacy.payload.SavedStateLoadedPayload; +import com.swirlds.platform.config.AddressBookConfig; +import com.swirlds.platform.config.BasicConfig; import com.swirlds.platform.config.StateConfig; import com.swirlds.platform.crypto.CryptoStatic; import com.swirlds.platform.internal.SignedStateLoadingException; import com.swirlds.platform.state.MerkleRoot; +import com.swirlds.platform.state.PlatformStateModifier; import com.swirlds.platform.state.snapshot.DeserializedSignedState; import com.swirlds.platform.state.snapshot.SavedStateInfo; import com.swirlds.platform.state.snapshot.SignedStateFilePath; @@ -44,6 +46,7 @@ import edu.umd.cs.findbugs.annotations.Nullable; import java.io.IOException; import java.io.UncheckedIOException; +import java.time.Instant; import java.util.List; import java.util.function.Supplier; import org.apache.logging.log4j.LogManager; @@ -59,8 +62,8 @@ public final class StartupStateUtils { private StartupStateUtils() {} /** - * Get the initial state to be used by this node. May return a state loaded from disk, or may return a genesis state - * if no valid state is found on disk. + * Used exclusively by {@link com.swirlds.platform.Browser} to get the initial state to be used by this node. + * May return a state loaded from disk, or may return a genesis state if no valid state is found on disk. * * @param configuration the configuration for this node * @param softwareVersion the software version of the app @@ -74,6 +77,7 @@ private StartupStateUtils() {} * delete malformed states */ @NonNull + @Deprecated(forRemoval = true) public static HashedReservedSignedState getInitialState( @NonNull final Configuration configuration, @NonNull final RecycleBin recycleBin, @@ -126,7 +130,7 @@ public static HashedReservedSignedState getInitialState( * delete malformed states */ @NonNull - static ReservedSignedState loadStateFile( + public static ReservedSignedState loadStateFile( @NonNull final Configuration configuration, @NonNull final RecycleBin recycleBin, @NonNull final NodeId selfId, @@ -305,4 +309,56 @@ private static void recycleState(@NonNull final RecycleBin recycleBin, @NonNull throw new UncheckedIOException("unable to recycle state", e); } } + + /** + * Build and initialize a genesis state. + * + * @param configuration the configuration for this node + * @param addressBook the current address book + * @param appVersion the software version of the app + * @param stateRoot the merkle root node of the state + * @return a reserved genesis signed state + */ + private static ReservedSignedState buildGenesisState( + @NonNull final Configuration configuration, + @NonNull final AddressBook addressBook, + @NonNull final SoftwareVersion appVersion, + @NonNull final MerkleRoot stateRoot) { + + if (!configuration.getConfigData(AddressBookConfig.class).useRosterLifecycle()) { + initGenesisPlatformState(configuration, stateRoot.getWritablePlatformState(), addressBook, appVersion); + } + + final SignedState signedState = new SignedState( + configuration, CryptoStatic::verifySignature, stateRoot, "genesis state", false, false, false); + return signedState.reserve("initial reservation on genesis state"); + } + + /** + * Initializes a genesis platform state. + * @param configuration the configuration for this node + * @param platformState the platform state to initialize + * @param addressBook the current address book + * @param appVersion the software version of the app + */ + private static void initGenesisPlatformState( + final Configuration configuration, + final PlatformStateModifier platformState, + final AddressBook addressBook, + final SoftwareVersion appVersion) { + platformState.bulkUpdate(v -> { + v.setAddressBook(addressBook.copy()); + v.setCreationSoftwareVersion(appVersion); + v.setRound(0); + v.setLegacyRunningEventHash(null); + v.setConsensusTimestamp(Instant.ofEpochSecond(0L)); + + final BasicConfig basicConfig = configuration.getConfigData(BasicConfig.class); + + final long genesisFreezeTime = basicConfig.genesisFreezeTime(); + if (genesisFreezeTime > 0) { + v.setFreezeTime(Instant.ofEpochSecond(genesisFreezeTime)); + } + }); + } } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/snapshot/DefaultStateSnapshotManager.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/snapshot/DefaultStateSnapshotManager.java index a992bd2052be..fc9f2066f4b0 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/snapshot/DefaultStateSnapshotManager.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/snapshot/DefaultStateSnapshotManager.java @@ -210,6 +210,10 @@ private void checkSignatures(@NonNull final SignedState reservedState) { // don't log an error if this is a freeze state. they are expected to lack signatures if (reservedState.isFreezeState()) { + final double signingWeightPercent = (((double) reservedState.getSigningWeight()) + / ((double) reservedState.getAddressBook().getTotalWeight())) + * 100.0; + logger.info( STATE_TO_DISK.getMarker(), """ @@ -219,10 +223,11 @@ private void checkSignatures(@NonNull final SignedState reservedState) { reservedState.getRound(), reservedState.getSigningWeight(), reservedState.getAddressBook().getTotalWeight(), - reservedState.getSigningWeight() - / reservedState.getAddressBook().getTotalWeight() - * 100.0); + signingWeightPercent); } else { + final double signingWeight1Percent = (((double) signingWeight1) / ((double) totalWeight1)) * 100.0; + final double signingWeight2Percent = (((double) signingWeight2) / ((double) totalWeight2)) * 100.0; + logger.error( EXCEPTION.getMarker(), new InsufficientSignaturesPayload( @@ -235,10 +240,10 @@ private void checkSignatures(@NonNull final SignedState reservedState) { reservedState.getRound(), signingWeight1, totalWeight1, - signingWeight1 / totalWeight1 * 100.0, + signingWeight1Percent, signingWeight2, totalWeight2, - signingWeight2 / totalWeight2 * 100.0, + signingWeight2Percent, Threshold.SUPER_MAJORITY.isSatisfiedBy(signingWeight1, totalWeight1), Threshold.SUPER_MAJORITY.isSatisfiedBy(signingWeight2, totalWeight2))))); } 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 8a6ccf77d000..899964b20678 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 @@ -32,7 +32,7 @@ import com.swirlds.platform.state.MerkleRoot; import com.swirlds.platform.state.service.PlatformStateService; import com.swirlds.platform.state.service.schemas.V0540PlatformStateSchema; -import com.swirlds.platform.state.service.schemas.V0540RosterSchema; +import com.swirlds.platform.state.service.schemas.V0540RosterBaseSchema; import com.swirlds.platform.state.signed.SignedState; import com.swirlds.state.State; import com.swirlds.state.lifecycle.Schema; @@ -189,7 +189,7 @@ public static void registerServiceStates(@NonNull final SignedState signedState) */ public static void registerServiceStates(@NonNull final State state) { registerServiceState(state, new V0540PlatformStateSchema(), PlatformStateService.NAME); - registerServiceState(state, new V0540RosterSchema(), RosterStateId.NAME); + registerServiceState(state, new V0540RosterBaseSchema(), RosterStateId.NAME); } private static void registerServiceState( diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/service/PlatformStateServiceTest.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/service/PlatformStateServiceTest.java index a73b5d0262ac..095a3567bb39 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/service/PlatformStateServiceTest.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/service/PlatformStateServiceTest.java @@ -17,7 +17,6 @@ package com.swirlds.platform.state.service; import static com.swirlds.platform.state.service.PlatformStateService.PLATFORM_STATE_SERVICE; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNull; @@ -25,10 +24,9 @@ import static org.mockito.BDDMockito.given; import com.hedera.hapi.node.base.SemanticVersion; -import com.hedera.hapi.node.state.roster.Roster; import com.hedera.hapi.platform.state.PlatformState; import com.swirlds.platform.state.service.schemas.V0540PlatformStateSchema; -import com.swirlds.platform.state.service.schemas.V057PlatformStateSchema; +import com.swirlds.platform.state.service.schemas.V058RosterLifecycleTransitionSchema; import com.swirlds.state.lifecycle.Schema; import com.swirlds.state.lifecycle.SchemaRegistry; import com.swirlds.state.merkle.MerkleStateRoot; @@ -50,12 +48,6 @@ class PlatformStateServiceTest { @Mock private SingletonNode platformState; - @Test - void canSetAndClearActiveRosterFn() { - assertDoesNotThrow(() -> PLATFORM_STATE_SERVICE.setActiveRosterFn(() -> Roster.DEFAULT)); - assertDoesNotThrow(PLATFORM_STATE_SERVICE::clearActiveRosterFn); - } - @Test void registersOneSchema() { final ArgumentCaptor captor = ArgumentCaptor.forClass(Schema.class); @@ -64,7 +56,7 @@ void registersOneSchema() { final var schemas = captor.getAllValues(); assertEquals(2, schemas.size()); assertInstanceOf(V0540PlatformStateSchema.class, schemas.getFirst()); - assertInstanceOf(V057PlatformStateSchema.class, schemas.getLast()); + assertInstanceOf(V058RosterLifecycleTransitionSchema.class, schemas.getLast()); } @Test diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/service/schemas/V057PlatformStateSchemaTest.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/service/schemas/V057PlatformStateSchemaTest.java deleted file mode 100644 index 5bcd3b4a80cc..000000000000 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/service/schemas/V057PlatformStateSchemaTest.java +++ /dev/null @@ -1,189 +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.swirlds.platform.state.service.schemas; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import com.hedera.hapi.node.base.SemanticVersion; -import com.hedera.hapi.node.state.roster.Roster; -import com.hedera.hapi.node.state.roster.RosterEntry; -import com.hedera.node.internal.network.Network; -import com.hedera.node.internal.network.NodeMetadata; -import com.hedera.pbj.runtime.io.buffer.Bytes; -import com.swirlds.config.api.Configuration; -import com.swirlds.platform.config.AddressBookConfig; -import com.swirlds.platform.state.service.WritablePlatformStateStore; -import com.swirlds.platform.system.BasicSoftwareVersion; -import com.swirlds.platform.system.SoftwareVersion; -import com.swirlds.state.lifecycle.MigrationContext; -import com.swirlds.state.lifecycle.StartupNetworks; -import com.swirlds.state.spi.WritableStates; -import edu.umd.cs.findbugs.annotations.NonNull; -import java.util.function.Function; -import java.util.function.Supplier; -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 V057PlatformStateSchemaTest { - private static final Network NETWORK = Network.newBuilder() - .nodeMetadata(NodeMetadata.newBuilder() - .rosterEntry(RosterEntry.newBuilder() - .nodeId(1L) - .gossipCaCertificate( - Bytes.fromHex( - "308203b130820219a003020102020900ad394d2f0a65e6f5300d06092a864886f70d01010c05003017311530130603550403130c732d7a77396a6566793079763020170d3030303130313030303030305a180f32313030303130313030303030305a3017311530130603550403130c732d7a77396a656679307976308201a2300d06092a864886f70d01010105000382018f003082018a0282018100c1a0ff5d2372b53d12d12bb87dd03f5e3427e0cee1d3c898bbd320c4b3dd17257944ea39a07f5344d9abfcdd50214072f1bbc12173fe7933d032c7d210734cc92d24be22b44cf50c2aa06f19bcd75180dc3e8dedd5ffcac02bf98721df9c3e79f20e9942cac9328b99160afea44d42c87b0147f3f29567085ed3f841dbe37aba35a2c5446bc638c62c703a6f680fa0601bfe7c6254e9fe2f471670ecdcca26128716a08f4141595ec0c4ac7ae589f37deede17480ecc1500f88335d0e33929725e8e4e775f3e4aa44c867bc86d3bf6d7165a4b766dd4ceb622221634a0a3d82840800b5b3e540640ea2f8c5749c3a6a0e0c474515c3f0ed9aadab8f84423a8954fd7f4e40b73125aeced4f791dba5052e3f5b3191a430f9b2dd30e4071cc54280c830da0d1e0dd54300c243ef08d9f81b3a90373f10910b6f4975bb2d861273993221e42b82b5af823267f79de90a7221129f0423724f9208a4ca15a73458c555e08e015db9d77c884acacaf4971d3854ea7bbdd9cfaf49df852c11473e96fa10203010001300d06092a864886f70d01010c05000382018100455e8d6c1b276d3d20a4b1ccc0abbbb36460cd985612d1068aab8cd5a479877f524c808c44469d3f17a752d7ce24d6e1536d5f02b8788890f6249c135ed05583126b6d38f7bf2d42c5a404f34379387d659eff5ff8a6ac1938254c2a3cee6abbbaca7b8e7069f7da8d5ce157a3c40bc0220abd05d8f54f0ac4aba3757a076ba14f598f1f835e566f71a50f933af979501499d959d356e71c5e954fdb0428578115fa540417b32156861c0f7960fa1e0473c76b2d579fbc30aa7ce718c7c413811b024f66d0e7e3350b30bd39a74f1f325818e5d26eececec78108bc77d55615b1568fb9b74e7567679606541d36f4f44c2bd07f5adb81c384d4b8ea1a287fbd356278344ec2582f040187f2f241ff812d54861754a47838b9cbe94f9d3e9333183c4f651a2e2ba3f5dcd77ff0560db17cb0d3481718d68aaafa076c6612674d3c264d42352811c2510a418987d1fba46ccaf5fe5d3b579fe002c106ffd4cd83ff0d0e16c9d92694a1764637d6fd2298fc1389c10de4e43b7fd1738d3acc13660")) - .build()) - .build()) - .build(); - - private static final Roster ROSTER = new Roster(NETWORK.nodeMetadata().stream() - .map(NodeMetadata::rosterEntryOrThrow) - .toList()); - - private static final SemanticVersion THEN = - SemanticVersion.newBuilder().major(7).build(); - - @Mock - private Supplier activeRosterSupplier; - - @Mock - private Supplier appVersionSupplier; - - @Mock - private MigrationContext migrationContext; - - @Mock - private Configuration configuration; - - @Mock - private WritableStates writableStates; - - @Mock - private Function platformStateStoreFactory; - - @Mock - private WritablePlatformStateStore platformStateStore; - - @Mock - private StartupNetworks startupNetworks; - - private V057PlatformStateSchema schema; - - @BeforeEach - void setUp() { - schema = new V057PlatformStateSchema(activeRosterSupplier, appVersionSupplier, platformStateStoreFactory); - } - - @Test - void noOpIfNotUsingRosterLifecycle() { - givenContextWith(CurrentVersion.NA, RosterLifecycle.OFF, AvailableNetwork.NONE); - - schema.restart(migrationContext); - - verify(migrationContext, never()).newStates(); - } - - @Test - void platformStateIsUpdatedOnGenesis() { - givenContextWith(CurrentVersion.NA, RosterLifecycle.ON, AvailableNetwork.GENESIS); - - schema.restart(migrationContext); - - verify(platformStateStore, times(1)).bulkUpdate(any()); - } - - @Test - void platformStateNotUpdatedIfNotUpgradeBoundary() { - givenContextWith(CurrentVersion.OLD, RosterLifecycle.ON, AvailableNetwork.NONE); - given(migrationContext.previousVersion()).willReturn(THEN); - - schema.restart(migrationContext); - - verify(platformStateStore, never()).bulkUpdate(any()); - } - - @Test - void platformStateIsUpdatedOnUpgradeBoundary() { - givenContextWith(CurrentVersion.NEW, RosterLifecycle.ON, AvailableNetwork.NONE); - given(migrationContext.previousVersion()).willReturn(THEN); - given(activeRosterSupplier.get()).willReturn(ROSTER); - - schema.restart(migrationContext); - - verify(platformStateStore, times(1)).bulkUpdate(any()); - } - - private enum CurrentVersion { - NA, - OLD, - NEW, - } - - private enum RosterLifecycle { - ON, - OFF - } - - private enum AvailableNetwork { - GENESIS, - NONE - } - - private void givenContextWith( - @NonNull final CurrentVersion currentVersion, - @NonNull final RosterLifecycle rosterLifecycle, - @NonNull final AvailableNetwork availableNetwork) { - switch (currentVersion) { - case NA -> { - // No-op - } - case OLD -> given(appVersionSupplier.get()).willReturn(new BasicSoftwareVersion(7)); - case NEW -> given(appVersionSupplier.get()).willReturn(new BasicSoftwareVersion(42)); - } - - given(migrationContext.configuration()).willReturn(configuration); - given(configuration.getConfigData(AddressBookConfig.class)) - .willReturn(new AddressBookConfig( - true, - false, - null, - 50, - switch (rosterLifecycle) { - case ON -> true; - case OFF -> false; - })); - - if (rosterLifecycle == RosterLifecycle.ON) { - given(migrationContext.newStates()).willReturn(writableStates); - given(platformStateStoreFactory.apply(writableStates)).willReturn(platformStateStore); - given(migrationContext.startupNetworks()).willReturn(startupNetworks); - } - - if (availableNetwork == AvailableNetwork.GENESIS) { - given(migrationContext.isGenesis()).willReturn(true); - given(startupNetworks.genesisNetworkOrThrow()).willReturn(NETWORK); - } - } -} diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/service/schemas/V058RosterLifecycleTransitionSchemaTest.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/service/schemas/V058RosterLifecycleTransitionSchemaTest.java new file mode 100644 index 000000000000..1ec3c0d8b7b1 --- /dev/null +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/service/schemas/V058RosterLifecycleTransitionSchemaTest.java @@ -0,0 +1,146 @@ +/* + * 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.service.schemas; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +import com.hedera.hapi.platform.state.PlatformState; +import com.swirlds.config.api.Configuration; +import com.swirlds.platform.config.AddressBookConfig; +import com.swirlds.platform.state.PlatformStateModifier; +import com.swirlds.platform.state.service.WritablePlatformStateStore; +import com.swirlds.platform.system.SoftwareVersion; +import com.swirlds.platform.system.address.AddressBook; +import com.swirlds.state.lifecycle.MigrationContext; +import com.swirlds.state.spi.WritableSingletonState; +import com.swirlds.state.spi.WritableStates; +import java.util.function.Consumer; +import java.util.function.Function; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class V058RosterLifecycleTransitionSchemaTest { + @Mock + private AddressBook addressBook; + + @Mock + private Configuration config; + + @Mock + private AddressBookConfig addressBookConfig; + + @Mock + private MigrationContext ctx; + + @Mock + private WritableStates writableStates; + + @Mock + private WritablePlatformStateStore platformStateStore; + + @Mock + private WritableSingletonState stateSingleton; + + @Mock + private Function appVersionFn; + + @Mock + private Function platformStateStoreFn; + + private V058RosterLifecycleTransitionSchema subject; + + @BeforeEach + void setUp() { + subject = new V058RosterLifecycleTransitionSchema(() -> addressBook, appVersionFn, platformStateStoreFn); + } + + @Test + void migrateNullsOutAddressBooksAtBoundary() { + final var oldState = PlatformState.newBuilder() + .previousAddressBook(com.hedera.hapi.platform.state.AddressBook.DEFAULT) + .addressBook(com.hedera.hapi.platform.state.AddressBook.DEFAULT) + .build(); + given(ctx.newStates()).willReturn(writableStates); + given(writableStates.getSingleton("PLATFORM_STATE")).willReturn(stateSingleton); + given(stateSingleton.get()).willReturn(oldState); + given(ctx.appConfig()).willReturn(config); + given(config.getConfigData(AddressBookConfig.class)).willReturn(addressBookConfig); + given(addressBookConfig.useRosterLifecycle()).willReturn(true); + subject.migrate(ctx); + verify(stateSingleton).put(PlatformState.DEFAULT); + } + + @Test + void doesNotApplyAtGenesis() { + given(ctx.appConfig()).willReturn(config); + given(config.getConfigData(AddressBookConfig.class)).willReturn(addressBookConfig); + given(ctx.isGenesis()).willReturn(true); + subject.restart(ctx); + verifyNoInteractions(platformStateStoreFn); + } + + @Test + void noOpIfAddressBookNotChanged() { + given(ctx.appConfig()).willReturn(config); + given(config.getConfigData(AddressBookConfig.class)).willReturn(addressBookConfig); + subject.restart(ctx); + verifyNoInteractions(platformStateStoreFn); + } + + @Test + @SuppressWarnings("unchecked") + void changesAtUpgradeBoundary() { + final ArgumentCaptor> captor = ArgumentCaptor.forClass(Consumer.class); + given(ctx.appConfig()).willReturn(config); + given(ctx.isUpgrade(any(), any())).willReturn(true); + given(config.getConfigData(AddressBookConfig.class)).willReturn(addressBookConfig); + given(addressBook.copy()).willReturn(addressBook); + given(ctx.newStates()).willReturn(writableStates); + given(platformStateStoreFn.apply(writableStates)).willReturn(platformStateStore); + given(platformStateStore.getAddressBook()).willReturn(addressBook); + subject.restart(ctx); + verify(platformStateStore).bulkUpdate(captor.capture()); + captor.getValue().accept(platformStateStore); + verify(platformStateStore).setPreviousAddressBook(addressBook); + verify(platformStateStore).setAddressBook(addressBook); + } + + @Test + @SuppressWarnings("unchecked") + void changesWhenForced() { + final ArgumentCaptor> captor = ArgumentCaptor.forClass(Consumer.class); + given(ctx.appConfig()).willReturn(config); + given(config.getConfigData(AddressBookConfig.class)).willReturn(addressBookConfig); + given(addressBookConfig.forceUseOfConfigAddressBook()).willReturn(true); + given(addressBook.copy()).willReturn(addressBook); + given(ctx.newStates()).willReturn(writableStates); + given(platformStateStoreFn.apply(writableStates)).willReturn(platformStateStore); + subject.restart(ctx); + verify(platformStateStore).bulkUpdate(captor.capture()); + captor.getValue().accept(platformStateStore); + verify(platformStateStore).setPreviousAddressBook(null); + verify(platformStateStore).setAddressBook(addressBook); + } +} diff --git a/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/state/FakeMerkleStateLifecycles.java b/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/state/FakeMerkleStateLifecycles.java index 9460420c1514..3a4040762dcc 100644 --- a/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/state/FakeMerkleStateLifecycles.java +++ b/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/state/FakeMerkleStateLifecycles.java @@ -35,11 +35,13 @@ import com.swirlds.merkledb.MerkleDbDataSourceBuilder; import com.swirlds.merkledb.MerkleDbTableConfig; import com.swirlds.merkledb.config.MerkleDbConfig; +import com.swirlds.platform.config.AddressBookConfig; +import com.swirlds.platform.config.BasicConfig; import com.swirlds.platform.state.MerkleStateLifecycles; import com.swirlds.platform.state.PlatformMerkleStateRoot; import com.swirlds.platform.state.service.PlatformStateService; import com.swirlds.platform.state.service.schemas.V0540PlatformStateSchema; -import com.swirlds.platform.state.service.schemas.V0540RosterSchema; +import com.swirlds.platform.state.service.schemas.V0540RosterBaseSchema; import com.swirlds.platform.system.BasicSoftwareVersion; import com.swirlds.platform.system.InitTrigger; import com.swirlds.platform.system.Platform; @@ -73,6 +75,8 @@ public enum FakeMerkleStateLifecycles implements MerkleStateLifecycles { FAKE_MERKLE_STATE_LIFECYCLES; public static final Configuration CONFIGURATION = ConfigurationBuilder.create() + .withConfigDataType(AddressBookConfig.class) + .withConfigDataType(BasicConfig.class) .withConfigDataType(MerkleDbConfig.class) .withConfigDataType(VirtualMapConfig.class) .withConfigDataType(TemporaryFileConfig.class) @@ -101,7 +105,7 @@ public static void registerMerkleStateRootClassIds() { VirtualNodeCache.class, () -> new VirtualNodeCache(CONFIGURATION.getConfigData(VirtualMapConfig.class)))); registerConstructablesForSchema(registry, new V0540PlatformStateSchema(), PlatformStateService.NAME); - registerConstructablesForSchema(registry, new V0540RosterSchema(), RosterStateId.NAME); + registerConstructablesForSchema(registry, new V0540RosterBaseSchema(), RosterStateId.NAME); } catch (ConstructableRegistryException e) { throw new IllegalStateException(e); } @@ -125,7 +129,7 @@ public List initPlatformState(@NonNull final State state) if (!(state instanceof MerkleStateRoot merkleStateRoot)) { throw new IllegalArgumentException("Can only be used with MerkleStateRoot instances"); } - final var schema = new V0540PlatformStateSchema(); + final var schema = new V0540PlatformStateSchema(config -> new BasicSoftwareVersion(1)); schema.statesToCreate().stream() .sorted(Comparator.comparing(StateDefinition::stateKey)) .forEach(def -> { @@ -155,7 +159,7 @@ public List initRosterState(@NonNull final State state) { if (!(state instanceof MerkleStateRoot merkleStateRoot)) { throw new IllegalArgumentException("Can only be used with MerkleStateRoot instances"); } - final var schema = new V0540RosterSchema(); + final var schema = new V0540RosterBaseSchema(); schema.statesToCreate().stream() .sorted(Comparator.comparing(StateDefinition::stateKey)) .forEach(def -> { diff --git a/platform-sdk/swirlds-state-api/src/main/java/com/swirlds/state/lifecycle/MigrationContext.java b/platform-sdk/swirlds-state-api/src/main/java/com/swirlds/state/lifecycle/MigrationContext.java index 495891b8920d..7b049977b728 100644 --- a/platform-sdk/swirlds-state-api/src/main/java/com/swirlds/state/lifecycle/MigrationContext.java +++ b/platform-sdk/swirlds-state-api/src/main/java/com/swirlds/state/lifecycle/MigrationContext.java @@ -24,6 +24,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.util.Map; +import java.util.function.Function; /** * Provides the context for a migration of state from one {@link Schema} version to another. @@ -55,14 +56,22 @@ public interface MigrationContext { WritableStates newStates(); /** - * The {@link Configuration} for this migration. Any portion of this configuration which was based on state (such - * as, in our case, file 121) will be current as of the previous state. This configuration is read-only. Having this - * configuration is useful for migrations that should behavior differently based on configuration. + * The app {@link Configuration} for this migration. Any portion of this configuration which was based on state + * (such as, in our case, file 121) will be current as of the previous state. This configuration is read-only. + * Having this configuration is useful for migrations that should behavior differently based on configuration. * - * @return The configuration to use. + * @return The application configuration to use. */ @NonNull - Configuration configuration(); + Configuration appConfig(); + + /** + * The platform {@link Configuration} for this migration. + * + * @return The platform configuration to use + */ + @NonNull + Configuration platformConfig(); /** * Information about the network itself. Generally, this is not useful information for migrations, but is used at @@ -101,6 +110,14 @@ public interface MigrationContext { @Nullable SemanticVersion previousVersion(); + /** + * Returns a mutable "scratchpad" that can be used to share values between different services + * during a migration. + * + * @return the shared values map + */ + Map sharedValues(); + /** * Returns whether this is a genesis migration. */ @@ -109,10 +126,19 @@ default boolean isGenesis() { } /** - * Returns a mutable "scratchpad" that can be used to share values between different services - * during a migration. - * - * @return the shared values map + * Returns whether the current version is an upgrade from the previous version, relative to the ordering + * implied by the given functions used to compare the version in the current app configuration and the + * previous state version. + * @param currentVersionFn the function to compute the current version from the app configuration + * @param previousVersionFn the function to compute the previous version from the saved state + * @return whether the current version is an upgrade from the previous version + * @param the type of the version */ - Map sharedValues(); + default > boolean isUpgrade( + @NonNull final Function currentVersionFn, + @NonNull final Function previousVersionFn) { + final var current = currentVersionFn.apply(appConfig()); + final var previous = previousVersion(); + return currentVersionFn.apply(appConfig()).compareTo(previousVersionFn.apply(previousVersion())) > 0; + } } diff --git a/platform-sdk/swirlds-state-api/src/main/java/com/swirlds/state/lifecycle/info/NetworkInfo.java b/platform-sdk/swirlds-state-api/src/main/java/com/swirlds/state/lifecycle/info/NetworkInfo.java index 9a535fcf002b..fd639069592a 100644 --- a/platform-sdk/swirlds-state-api/src/main/java/com/swirlds/state/lifecycle/info/NetworkInfo.java +++ b/platform-sdk/swirlds-state-api/src/main/java/com/swirlds/state/lifecycle/info/NetworkInfo.java @@ -16,7 +16,6 @@ package com.swirlds.state.lifecycle.info; -import com.hedera.hapi.node.state.roster.Roster; import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.state.State; import edu.umd.cs.findbugs.annotations.NonNull; @@ -58,10 +57,4 @@ public interface NetworkInfo { * @param state the state to update from */ void updateFrom(State state); - - /** - * Returns the currently active roster used by the network. - * @return the currently active roster - */ - Roster roster(); } diff --git a/platform-sdk/swirlds-state-api/src/main/java/com/swirlds/state/lifecycle/info/NodeInfo.java b/platform-sdk/swirlds-state-api/src/main/java/com/swirlds/state/lifecycle/info/NodeInfo.java index ef8e203cc9ef..98974dbc132c 100644 --- a/platform-sdk/swirlds-state-api/src/main/java/com/swirlds/state/lifecycle/info/NodeInfo.java +++ b/platform-sdk/swirlds-state-api/src/main/java/com/swirlds/state/lifecycle/info/NodeInfo.java @@ -34,12 +34,12 @@ public interface NodeInfo { /** - * Convenience method to check if this node is zero-stake. + * Convenience method to check if this node has zero weight. * - * @return whether this node has zero stake. + * @return whether this node has zero weight */ - default boolean zeroStake() { - return stake() == 0; + default boolean zeroWeight() { + return weight() == 0; } /** @@ -65,7 +65,7 @@ default boolean zeroStake() { * The stake weight of this node. * @return the stake weight */ - long stake(); + long weight(); /** * The signing x509 certificate bytes of the member @@ -82,22 +82,26 @@ default boolean zeroStake() { List gossipEndpoints(); /** - * The public key of this node, as a hex-encoded string. It is extracted from the certificate bytes. - * - * @return the public key + * The gossip X.509 certificate of this node. + * @return the gossip X.509 certificate + * @throws IllegalStateException if the certificate could not be extracted */ - default String hexEncodedPublicKey() { + default X509Certificate sigCert() { try { CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); - - // Convert the byte array to an InputStream and generate the X509Certificate object - X509Certificate certificate = (X509Certificate) certificateFactory.generateCertificate( + return (X509Certificate) certificateFactory.generateCertificate( new ByteArrayInputStream(sigCertBytes().toByteArray())); - - // Return the public key from the certificate - return CommonUtils.hex(certificate.getPublicKey().getEncoded()); } catch (CertificateException e) { throw new IllegalStateException("Error extracting public key from certificate", e); } } + + /** + * The public key of this node, as a hex-encoded string. It is extracted from the certificate bytes. + * + * @return the public key + */ + default String hexEncodedPublicKey() { + return CommonUtils.hex(sigCert().getPublicKey().getEncoded()); + } } diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/preconsensus/PcesFileTests.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/preconsensus/PcesFileTests.java index c52331db51ad..25b49e703b32 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/preconsensus/PcesFileTests.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/preconsensus/PcesFileTests.java @@ -258,7 +258,11 @@ void deletionTest(@NonNull final AncientMode ancientMode) throws IOException { final Instant now = Instant.now(); // When we start out, the test directory should be empty. - assertEquals(0, Files.list(testDirectory).count()); + int filesCount; + try (Stream list = Files.list(testDirectory)) { + filesCount = (int) list.count(); + } + assertEquals(0, filesCount, "Unexpected number of files: " + filesCount); final List times = new ArrayList<>(); times.add(now); @@ -306,7 +310,10 @@ void deletionTest(@NonNull final AncientMode ancientMode) throws IOException { } // After all files have been deleted, the test directory should be empty again. - assertEquals(0, Files.list(testDirectory).count()); + try (Stream list = Files.list(testDirectory)) { + filesCount = (int) list.count(); + } + assertEquals(0, filesCount, "Unexpected number of files: " + filesCount); } @SuppressWarnings("resource") @@ -332,7 +339,11 @@ void recycleTest(@NonNull final AncientMode ancientMode) throws IOException { Files.createDirectories(recycleDirectory); // When we start out, the test directory should be empty. - assertEquals(0, Files.list(streamDirectory).count()); + int filesCount; + try (Stream list = Files.list(streamDirectory)) { + filesCount = (int) list.count(); + } + assertEquals(0, filesCount, "Unexpected number of files: " + filesCount); final List times = new ArrayList<>(); times.add(now); @@ -380,7 +391,10 @@ void recycleTest(@NonNull final AncientMode ancientMode) throws IOException { } // After all files have been deleted, the test directory should be empty again. - assertEquals(0, Files.list(streamDirectory).count()); + try (Stream list = Files.list(streamDirectory)) { + filesCount = (int) list.count(); + } + assertEquals(0, filesCount, "Unexpected number of files: " + filesCount); // All files should have been moved to the recycle directory for (final PcesFile file : files) { diff --git a/version.txt b/version.txt index 46448c71b9df..a60476bfe1c7 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.57.0 +0.58.0