diff --git a/.github/workflows/zxc-build-library.yaml b/.github/workflows/zxc-build-library.yaml index 31ba4b16..80422610 100644 --- a/.github/workflows/zxc-build-library.yaml +++ b/.github/workflows/zxc-build-library.yaml @@ -183,15 +183,23 @@ jobs: uses: hiero-ledger/hiero-solo-action@fbca3e7a99ce9aa8a250563a81187abe115e0dad # v0.16.0 with: installMirrorNode: true - hieroVersion: v0.65.0 - + mirrorNodeVersion: v0.142.0 + hieroVersion: v0.68.4 + dualMode: true - name: Start CTest suite (Debug) - run: ${{ steps.cgroup.outputs.exec }} ctest -j 6 -C Debug --test-dir build/${{ matrix.preset }}-debug --output-on-failure + run: ${{ steps.cgroup.outputs.exec }} ctest -j 6 -C Debug --test-dir build/${{ matrix.preset }}-debug -E NodeUpdateTransactionIntegrationTests --output-on-failure - name: Start CTest suite (Release) if: github.event.pull_request.merged == true - run: ${{ steps.cgroup.outputs.exec }} ctest -j 6 -C Debug --test-dir build/${{ matrix.preset }}-release --output-on-failure + run: ${{ steps.cgroup.outputs.exec }} ctest -j 6 -C Debug --test-dir build/${{ matrix.preset }}-release -E NodeUpdateTransactionIntegrationTests --output-on-failure + + - name: Run Node Update Integration Tests (Debug) + run: ${{ steps.cgroup.outputs.exec }} ctest -C Debug --test-dir build/${{ matrix.preset }}-debug -R NodeUpdateTransactionIntegrationTests --output-on-failure + + - name: Run Node Update Integration Tests (Release) + if: github.event.pull_request.merged == true + run: ${{ steps.cgroup.outputs.exec }} ctest -C Debug --test-dir build/${{ matrix.preset }}-release -R NodeUpdateTransactionIntegrationTests --output-on-failure - name: Compute Short SHA id: sha diff --git a/.gitignore b/.gitignore index 41633e87..a37d6355 100644 --- a/.gitignore +++ b/.gitignore @@ -889,6 +889,7 @@ src/proto/*.proto /proto/event /proto/sdk /proto/platform +/proto/block ### Ignore Generated Package Files package diff --git a/HieroApi.cmake b/HieroApi.cmake index 8d303649..1abc06a8 100644 --- a/HieroApi.cmake +++ b/HieroApi.cmake @@ -1,8 +1,8 @@ -set(HAPI_VERSION_TAG "v0.62.0" CACHE STRING "Use the configured version tag for the Hiero API protobufs") -set(HAPI_COMMIT_HASH "e7db7cd74e1709ca719d1fcc9119aa062e82930f" CACHE STRING "Use the configured commit hash for the Hiero API protobufs (overrides version tag if provided)") +set(HAPI_VERSION_TAG "v0.68.1-rc.1" CACHE STRING "Use the configured version tag for the Hiero API protobufs") +set(HAPI_COMMIT_HASH "fadd38a6b2badec02bee35272f03fe8fafadea00" CACHE STRING "Use the configured commit hash for the Hiero API protobufs (overrides version tag if provided)") if (HAPI_VERSION_TAG STREQUAL "") - set(HAPI_VERSION_TAG "v0.62.0") + set(HAPI_VERSION_TAG "v0.68.1-rc.1") endif () # Use commit hash if provided, otherwise use version tag @@ -61,6 +61,7 @@ file(INSTALL ${PROJECT_SOURCE_DIR}/proto/service-external-proto/sdk/ DESTINATION file(INSTALL ${hproto_SOURCE_DIR}/hapi/hedera-protobuf-java-api/src/main/proto/platform/ DESTINATION ${PROTO_SRC}/platform) file(INSTALL ${hproto_SOURCE_DIR}/hapi/hedera-protobuf-java-api/src/main/proto/services/ DESTINATION ${PROTO_SRC}/services) +file(INSTALL ${hproto_SOURCE_DIR}/hapi/hedera-protobuf-java-api/src/main/proto/block/ DESTINATION ${PROTO_SRC}/block) add_subdirectory(proto) diff --git a/proto/CMakeLists.txt b/proto/CMakeLists.txt index 4390bd25..c4e907af 100644 --- a/proto/CMakeLists.txt +++ b/proto/CMakeLists.txt @@ -1,5 +1,6 @@ # Begin Protobuf Definitions set(PROTO_FILES + block/stream/chain_of_trust_proof.proto transaction_list.proto mirror/consensus_service.proto @@ -56,6 +57,9 @@ set(PROTO_FILES services/get_account_details.proto services/get_by_key.proto services/get_by_solidity_id.proto + services/hook_dispatch.proto + services/hook_types.proto + services/lambda_sstore.proto services/network_get_execution_time.proto services/network_get_version_info.proto services/network_service.proto diff --git a/src/sdk/main/include/Client.h b/src/sdk/main/include/Client.h index 0856664d..45722ae0 100644 --- a/src/sdk/main/include/Client.h +++ b/src/sdk/main/include/Client.h @@ -357,6 +357,14 @@ class Client */ Client& setNetworkFromAddressBook(const NodeAddressBook& addressBook); + /** + * Update the address book from the network immediately. + * This method is typically called when a node returns INVALID_NODE_ACCOUNT. + * The update is done synchronously to ensure the network has the latest nodes + * before retrying with another node. + */ + void updateAddressBook(); + /** * Set the consensus network with which this Client should communicate. * diff --git a/src/sdk/main/include/Executable.h b/src/sdk/main/include/Executable.h index fdad984e..338d7f8a 100644 --- a/src/sdk/main/include/Executable.h +++ b/src/sdk/main/include/Executable.h @@ -272,6 +272,13 @@ class Executable Executable(Executable&&) noexcept = default; Executable& operator=(Executable&&) noexcept = default; + /** + * Get the maximum number of attempts that have been explicitly set for this Executable. + * + * @return The maximum number of attempts, or std::nullopt if not set. + */ + [[nodiscard]] inline std::optional getMaxAttemptsSet() const { return mMaxAttempts; } + /** * Enumeration describing the status of a submitted Executable. */ @@ -292,7 +299,11 @@ class Executable /** * The call was successful but an operation did not complete. */ - RETRY + RETRY, + /** + * The node account is invalid; retry with another node and update address book. + */ + RETRY_WITH_ANOTHER_NODE }; /** diff --git a/src/sdk/main/include/Status.h b/src/sdk/main/include/Status.h index d2ac6ce5..13fa7e60 100644 --- a/src/sdk/main/include/Status.h +++ b/src/sdk/main/include/Status.h @@ -1855,7 +1855,12 @@ enum class Status /** * An NFT transfers list referenced a token type other than NON_FUNGIBLE_UNIQUE. */ - NFT_TRANSFERS_ONLY_ALLOWED_FOR_NON_FUNGIBLE_UNIQUE + NFT_TRANSFERS_ONLY_ALLOWED_FOR_NON_FUNGIBLE_UNIQUE, + + /** + * The node account has a zero balance. + */ + NODE_ACCOUNT_HAS_ZERO_BALANCE, }; /** diff --git a/src/sdk/main/include/impl/BaseNetwork.h b/src/sdk/main/include/impl/BaseNetwork.h index 40182f19..1e57bb3f 100644 --- a/src/sdk/main/include/impl/BaseNetwork.h +++ b/src/sdk/main/include/impl/BaseNetwork.h @@ -211,6 +211,17 @@ class BaseNetwork return mNetwork; } + /** + * Set the internal network map. This should be used carefully when updating node keys. + * + * @param network The new network map. + */ + inline void setNetworkInternal( + const std::unordered_map>>& network) + { + mNetwork = network; + } + /** * Get the list of NodeTypes on this BaseNetwork. * diff --git a/src/sdk/main/include/impl/Network.h b/src/sdk/main/include/impl/Network.h index 7e1013a6..ce47a579 100644 --- a/src/sdk/main/include/impl/Network.h +++ b/src/sdk/main/include/impl/Network.h @@ -68,6 +68,17 @@ class Network : public BaseNetwork const NodeAddressBook& addressBook, unsigned int port); + /** + * Update node account IDs from an address book without closing connections. + * This is used when nodes' account IDs change (e.g., during node update transactions). + * Nodes are matched by their IP address, and their AccountIds are updated in place. + * + * @param addressBook The address book containing the updated node account IDs. + * @param port The port to use when matching nodes. + * @return A reference to this Network object with updated node account IDs. + */ + Network& updateNodeAccountIds(const NodeAddressBook& addressBook, unsigned int port); + /** * Derived from BaseNetwork. Set the ledger ID of this Network. * @@ -118,12 +129,13 @@ class Network : public BaseNetwork [[nodiscard]] unsigned int getNumberOfNodesForRequest() const; /** - * Get a list of node account IDs on which to execute. This will pick 1/3 of the available nodes sorted by health and + * Get a list of node account IDs on which to execute. This will return up to maxAttempts nodes sorted by health and * expected delay from the network. * + * @param maxAttempts The maximum number of attempts that will be made for this execution. * @return A list of AccountIds that are running nodes on which should be executed. */ - [[nodiscard]] std::vector getNodeAccountIdsForExecute(); + [[nodiscard]] std::vector getNodeAccountIdsForExecute(unsigned int maxAttempts); /** * Get a map of this Network, mapping the Node addresses to their AccountIds. diff --git a/src/sdk/main/include/impl/Node.h b/src/sdk/main/include/impl/Node.h index 533487e2..2030885e 100644 --- a/src/sdk/main/include/impl/Node.h +++ b/src/sdk/main/include/impl/Node.h @@ -126,6 +126,19 @@ class Node : public BaseNode return mAccountId; }; + /** + * Update the AccountId of this Node. + * This is used when a node's account ID changes (e.g., during a node update transaction) + * without needing to close and recreate the connection. + * + * @param accountId The new AccountId for this Node. + */ + inline void setAccountId(const AccountId& accountId) + { + std::unique_lock lock(*getLock()); + mAccountId = accountId; + }; + /** * Get the node certificate hash of this Node. * diff --git a/src/sdk/main/src/Client.cc b/src/sdk/main/src/Client.cc index 9f1a4d90..140507b6 100644 --- a/src/sdk/main/src/Client.cc +++ b/src/sdk/main/src/Client.cc @@ -679,6 +679,30 @@ Client& Client::setNetworkFromAddressBook(const NodeAddressBook& addressBook) return *this; } +//----- +void Client::updateAddressBook() +{ + try + { + mImpl->mLogger.trace("Updating address book after INVALID_NODE_ACCOUNT response"); + + // Get the address book - do NOT hold the mutex during query execution + // as execute() will call other Client methods that also need the mutex + const NodeAddressBook addressBook = AddressBookQuery().setFileId(FileId::ADDRESS_BOOK).execute(*this); + + // Only acquire the mutex for the actual update + std::unique_lock lock(mImpl->mMutex); + setNetworkFromAddressBookInternal(addressBook); + + mImpl->mLogger.trace("Address book successfully updated"); + } + catch (const std::exception& exception) + { + mImpl->mLogger.warn(std::string("Failed to update address book: ") + exception.what()); + throw; + } +} + //----- Client& Client::setNetwork(const std::unordered_map& networkMap) { @@ -1073,11 +1097,26 @@ std::shared_ptr Client::getClientMirrorNetwork() const //----- void Client::setNetworkFromAddressBookInternal(const NodeAddressBook& addressBook) { - mImpl->mNetwork->setNetwork(internal::Network::getNetworkFromAddressBook( - addressBook, - mImpl->mNetwork->isTransportSecurity() == internal::TLSBehavior::REQUIRE - ? internal::BaseNodeAddress::PORT_NODE_TLS - : internal::BaseNodeAddress::PORT_NODE_PLAIN)); + // First try the port based on TLS setting + unsigned int preferredPort = mImpl->mNetwork->isTransportSecurity() == internal::TLSBehavior::REQUIRE + ? internal::BaseNodeAddress::PORT_NODE_TLS + : internal::BaseNodeAddress::PORT_NODE_PLAIN; + + // Try to update node account IDs without closing connections + mImpl->mNetwork->updateNodeAccountIds(addressBook, preferredPort); + + // If that didn't find any matches, try the other port + // (This handles the case where the TLS setting doesn't match the actual ports) + auto networkMap = internal::Network::getNetworkFromAddressBook(addressBook, preferredPort); + + if (networkMap.empty()) + { + unsigned int alternatePort = (preferredPort == internal::BaseNodeAddress::PORT_NODE_TLS) + ? internal::BaseNodeAddress::PORT_NODE_PLAIN + : internal::BaseNodeAddress::PORT_NODE_TLS; + + mImpl->mNetwork->updateNodeAccountIds(addressBook, alternatePort); + } } //----- diff --git a/src/sdk/main/src/Executable.cc b/src/sdk/main/src/Executable.cc index 1b159443..a2631d48 100644 --- a/src/sdk/main/src/Executable.cc +++ b/src/sdk/main/src/Executable.cc @@ -123,7 +123,7 @@ SdkResponseType Executable> nodes = getNodesFromNodeAccountIds(client); + std::vector> nodes = getNodesFromNodeAccountIds(client); // The time to timeout. const std::chrono::system_clock::time_point timeoutTime = std::chrono::system_clock::now() + timeout; @@ -131,8 +131,43 @@ SdkResponseType Executable, Status> nodeResponses; + // Flag to track if we need to update the address book, and count how many times we've updated + bool needsAddressBookUpdate = false; + unsigned int addressBookUpdateCount = 0; + constexpr unsigned int MAX_ADDRESS_BOOK_UPDATES = 1; // Only update once per transaction + for (unsigned int attempt = 0U;; ++attempt) { + // If we flagged that we need an address book update, do it now before this attempt + // This uses updateNodeAccountIds which doesn't close connections, so it's safe + // But only do it once to avoid infinite loops if the addresses don't match + if (needsAddressBookUpdate && addressBookUpdateCount < MAX_ADDRESS_BOOK_UPDATES) + { + // Only update address book if a mirror network is configured + if (!client.getMirrorNetwork().empty()) + { + const_cast(client).updateAddressBook(); + + // Refetch nodes after updating address book + // The node account IDs may have been updated, so we need to get fresh Node objects + try + { + nodes = getNodesFromNodeAccountIds(client); + } + catch (const IllegalStateException&) + { + // If we can't find any nodes for the requested AccountIds after updating the address book, + // it means those AccountIds are no longer valid in the network. + // Throw PrecheckStatusException with INVALID_NODE_ACCOUNT status. + throw PrecheckStatusException(Status::INVALID_NODE_ACCOUNT, getTransactionIdInternal()); + } + + nodeResponses.clear(); + } + + needsAddressBookUpdate = false; + addressBookUpdateCount++; + } // Get the timeout for the current attempt. std::chrono::system_clock::time_point attemptTimeout = std::chrono::system_clock::now() + mCurrentGrpcDeadline; if (attemptTimeout > timeoutTime) @@ -232,6 +267,29 @@ SdkResponseType ExecutablegetAccountId().toString() + " during attempt #" + std::to_string(attempt)); + + // Mark this node as unhealthy + node->increaseBackoff(); + + // Check if we've tried all available unique nodes and they all returned INVALID_NODE_ACCOUNT + // If we've already updated the address book, there's no point in retrying - throw immediately + if (nodeResponses.size() >= nodes.size() && addressBookUpdateCount >= MAX_ADDRESS_BOOK_UPDATES) + { + throw PrecheckStatusException(Status::INVALID_NODE_ACCOUNT, getTransactionIdInternal()); + } + + // Flag that we need to update the address book before the next attempt + // This will update node account IDs without closing connections + needsAddressBookUpdate = true; + + // Continue with other nodes + continue; + } // Response isn't ready yet from the network case ExecutionStatus::RETRY: { @@ -437,6 +495,8 @@ Executable case Status::PLATFORM_NOT_ACTIVE: case Status::BUSY: return ExecutionStatus::SERVER_ERROR; + case Status::INVALID_NODE_ACCOUNT: + return ExecutionStatus::RETRY_WITH_ANOTHER_NODE; case Status::OK: return ExecutionStatus::SUCCESS; // Let derived class handle this status, assume request error @@ -472,40 +532,33 @@ std::vector> Executable::getNodesFromNodeAccountIds( const Client& client) const { - std::vector> nodes; + // Collect all available nodes from all the requested AccountIds + std::vector> availableNodes; - // If there are multiple nodes, this Executable should simply try a different node. for (const AccountId& accountId : mNodeAccountIds) { const std::vector> nodeProxies = client.getClientNetwork()->getNodeProxies(accountId); - // Verify the node account ID mapped to a valid Node. - if (nodeProxies.empty()) - { - continue; - } - - // If there is a single node account id add all it's corresponding proxies - if (mNodeAccountIds.size() == 1) + // Add all proxies for this AccountId to the available nodes list + if (!nodeProxies.empty()) { - nodes.insert(nodes.end(), nodeProxies.begin(), nodeProxies.end()); - break; + // Pick one random proxy from this AccountId's proxies + availableNodes.push_back( + nodeProxies.at(internal::Utilities::getRandomNumber(0U, static_cast(nodeProxies.size()) - 1U))); } - - // Pick a random proxy from the proxy list to add to the node list. - nodes.push_back( - nodeProxies.at(internal::Utilities::getRandomNumber(0U, static_cast(nodeProxies.size()) - 1U))); } - if (nodes.empty()) + if (availableNodes.empty()) { throw IllegalStateException( "Attempting to fetch node proxies for which are not included in the Client's network. Please review your Client " "configuration or the set node account id."); } - return nodes; + // Return the available nodes without duplicating them + // The retry logic in getNodeIndexForExecute will cycle through them using attempt % nodes.size() + return availableNodes; } //----- diff --git a/src/sdk/main/src/NodeUpdateTransaction.cc b/src/sdk/main/src/NodeUpdateTransaction.cc index 13003474..c06cf2a2 100644 --- a/src/sdk/main/src/NodeUpdateTransaction.cc +++ b/src/sdk/main/src/NodeUpdateTransaction.cc @@ -222,7 +222,10 @@ aproto::NodeUpdateTransactionBody* NodeUpdateTransaction::build() const body->mutable_service_endpoint()->AddAllocated(e.toProtobuf().release()); } - body->mutable_gossip_ca_certificate()->set_value(internal::Utilities::byteVectorToString(mGossipCaCertificate)); + if (!mGossipCaCertificate.empty()) + { + body->mutable_gossip_ca_certificate()->set_value(internal::Utilities::byteVectorToString(mGossipCaCertificate)); + } if (mGrpcCertificateHash.has_value()) { diff --git a/src/sdk/main/src/Query.cc b/src/sdk/main/src/Query.cc index f15a5598..32a7678c 100644 --- a/src/sdk/main/src/Query.cc +++ b/src/sdk/main/src/Query.cc @@ -12,6 +12,7 @@ #include "ContractFunctionResult.h" #include "ContractInfo.h" #include "ContractInfoQuery.h" +#include "Defaults.h" #include "FileContentsQuery.h" #include "FileInfo.h" #include "FileInfoQuery.h" @@ -321,9 +322,19 @@ void Query::onExecute(const Client& client) throw UninitializedException("Client has not been initialized with a valid network"); } - // Have the Client's network generate the node account IDs to which to send this Query. + // Determine maxAttempts using the same fallback hierarchy as setExecutionParameters(): + // 1. Query's explicitly set maxAttempts + // 2. Client's explicitly set maxAttempts + // 3. DEFAULT_MAX_ATTEMPTS constant + const std::optional executableMaxAttempts = + Executable::getMaxAttemptsSet(); + const unsigned int maxAttempts = executableMaxAttempts.has_value() ? executableMaxAttempts.value() + : client.getMaxAttempts().has_value() ? client.getMaxAttempts().value() + : DEFAULT_MAX_ATTEMPTS; + + // Request up to maxAttempts most healthy nodes from the network for this Query Executable::setNodeAccountIds( - client.getClientNetwork()->getNodeAccountIdsForExecute()); + client.getClientNetwork()->getNodeAccountIdsForExecute(maxAttempts)); } // Validate checksums if that option is enabled. diff --git a/src/sdk/main/src/Status.cc b/src/sdk/main/src/Status.cc index 92f54fad..40c3281d 100644 --- a/src/sdk/main/src/Status.cc +++ b/src/sdk/main/src/Status.cc @@ -385,7 +385,8 @@ const std::unordered_map gProtobufResponseCodeT Status::AIRDROP_CONTAINS_MULTIPLE_SENDERS_FOR_A_TOKEN }, { proto::ResponseCodeEnum::GRPC_WEB_PROXY_NOT_SUPPORTED, Status::GRPC_WEB_PROXY_NOT_SUPPORTED }, { proto::ResponseCodeEnum::NFT_TRANSFERS_ONLY_ALLOWED_FOR_NON_FUNGIBLE_UNIQUE, - Status::NFT_TRANSFERS_ONLY_ALLOWED_FOR_NON_FUNGIBLE_UNIQUE } + Status::NFT_TRANSFERS_ONLY_ALLOWED_FOR_NON_FUNGIBLE_UNIQUE }, + { proto::ResponseCodeEnum::NODE_ACCOUNT_HAS_ZERO_BALANCE, Status::NODE_ACCOUNT_HAS_ZERO_BALANCE }, }; //----- @@ -764,10 +765,11 @@ const std::unordered_map gStatusToProtobufRespo { Status::CREATING_SYSTEM_ENTITIES, proto::ResponseCodeEnum::CREATING_SYSTEM_ENTITIES }, { Status::THROTTLE_GROUP_LCM_OVERFLOW, proto::ResponseCodeEnum::THROTTLE_GROUP_LCM_OVERFLOW }, { Status::AIRDROP_CONTAINS_MULTIPLE_SENDERS_FOR_A_TOKEN, - proto::ResponseCodeEnum::AIRDROP_CONTAINS_MULTIPLE_SENDERS_FOR_A_TOKEN }, + proto::ResponseCodeEnum::AIRDROP_CONTAINS_MULTIPLE_SENDERS_FOR_A_TOKEN }, { Status::GRPC_WEB_PROXY_NOT_SUPPORTED, proto::ResponseCodeEnum::GRPC_WEB_PROXY_NOT_SUPPORTED }, { Status::NFT_TRANSFERS_ONLY_ALLOWED_FOR_NON_FUNGIBLE_UNIQUE, - proto::ResponseCodeEnum::NFT_TRANSFERS_ONLY_ALLOWED_FOR_NON_FUNGIBLE_UNIQUE } + proto::ResponseCodeEnum::NFT_TRANSFERS_ONLY_ALLOWED_FOR_NON_FUNGIBLE_UNIQUE }, + { Status::NODE_ACCOUNT_HAS_ZERO_BALANCE, proto::ResponseCodeEnum::NODE_ACCOUNT_HAS_ZERO_BALANCE }, }; //----- @@ -1125,7 +1127,8 @@ const std::unordered_map gStatusToString = { "AIRDROP_CONTAINS_MULTIPLE_SENDERS_FOR_A_TOKEN" }, { Status::GRPC_WEB_PROXY_NOT_SUPPORTED, "GRPC_WEB_PROXY_NOT_SUPPORTED" }, { Status::NFT_TRANSFERS_ONLY_ALLOWED_FOR_NON_FUNGIBLE_UNIQUE, - "NFT_TRANSFERS_ONLY_ALLOWED_FOR_NON_FUNGIBLE_UNIQUE" } + "NFT_TRANSFERS_ONLY_ALLOWED_FOR_NON_FUNGIBLE_UNIQUE" }, + { Status::NODE_ACCOUNT_HAS_ZERO_BALANCE, "NODE_ACCOUNT_HAS_ZERO_BALANCE" }, }; } // namespace Hiero \ No newline at end of file diff --git a/src/sdk/main/src/Transaction.cc b/src/sdk/main/src/Transaction.cc index 7c036d70..c10b7455 100644 --- a/src/sdk/main/src/Transaction.cc +++ b/src/sdk/main/src/Transaction.cc @@ -580,10 +580,20 @@ SdkRequestType& Transaction::freezeWith(const Client* client) throw UninitializedException("Client has not been initialized with a valid network."); } - // Have the Client's network generate the node account IDs to which to send - // this Transaction. + // Determine maxAttempts using the same fallback hierarchy as setExecutionParameters(): + // 1. Transaction's explicitly set maxAttempts + // 2. Client's explicitly set maxAttempts + // 3. DEFAULT_MAX_ATTEMPTS constant + const std::optional executableMaxAttempts = + Executable:: + getMaxAttemptsSet(); + const unsigned int maxAttempts = executableMaxAttempts.has_value() ? executableMaxAttempts.value() + : client->getMaxAttempts().has_value() ? client->getMaxAttempts().value() + : DEFAULT_MAX_ATTEMPTS; + + // Request up to maxAttempts most healthy nodes from the network for this Transaction Executable::setNodeAccountIds( - client->getClientNetwork()->getNodeAccountIdsForExecute()); + client->getClientNetwork()->getNodeAccountIdsForExecute(maxAttempts)); } // Regenerate the final SignedTransaction protobuf objects. diff --git a/src/sdk/main/src/impl/Network.cc b/src/sdk/main/src/impl/Network.cc index dfa2cc95..172827bc 100644 --- a/src/sdk/main/src/impl/Network.cc +++ b/src/sdk/main/src/impl/Network.cc @@ -13,6 +13,32 @@ namespace Hiero::internal { +namespace +{ +/** + * Map Kubernetes service DNS names to correct ports for local development port-forwarding. + * This handles scenarios where network-node2 is port-forwarded to 51211 while network-node1 stays at 50211. + * The DNS names resolve to 127.0.0.1 via /etc/hosts entries. + * + * @param endpoint The endpoint address to potentially map + * @return The mapped endpoint with corrected port if local development, otherwise the original endpoint unchanged + * + * @note This function ONLY maps specific local development Kubernetes service DNS names. + * All other addresses (testnet, mainnet, previewnet, custom networks) pass through unchanged. + */ +std::string mapEndpointForLocalDevelopment(const std::string& endpoint) +{ + // Map network-node2 from port 50211 to 51211 for local port-forwarding + if (endpoint.find("network-node2-svc.solo.svc.cluster.local:50211") != std::string::npos) + { + return "network-node2-svc.solo.svc.cluster.local:51211"; + } + + // All other addresses (including network-node1) pass through unchanged + return endpoint; +} +} // anonymous namespace + //----- Network Network::forMainnet() { @@ -48,7 +74,9 @@ std::unordered_map Network::getNetworkFromAddressBook(co { if (endpoint.getPort() == port) { - network[endpoint.toString()] = nodeAddress.getAccountId(); + // Map the endpoint for local development (e.g., Kubernetes service names to localhost) + const std::string mappedEndpoint = mapEndpointForLocalDevelopment(endpoint.toString()); + network[mappedEndpoint] = nodeAddress.getAccountId(); } } } @@ -56,6 +84,76 @@ std::unordered_map Network::getNetworkFromAddressBook(co return network; } +//----- +Network& Network::updateNodeAccountIds(const NodeAddressBook& addressBook, unsigned int port) +{ + std::unique_lock lock(*getLock()); + + try + { + // Build a map of full addresses (with port) to new AccountIds from the address book + std::unordered_map addressToAccountId; + + for (const auto& nodeAddress : addressBook.getNodeAddresses()) + { + for (const auto& endpoint : nodeAddress.getEndpoints()) + { + if (endpoint.getPort() == port) + { + // Map the endpoint for local development (e.g., Kubernetes service names to localhost) + const std::string mappedEndpoint = mapEndpointForLocalDevelopment(endpoint.toString()); + addressToAccountId[mappedEndpoint] = nodeAddress.getAccountId(); + } + } + } + + // Build a new network map with updated AccountIds + std::unordered_map>> newNetworkMap; + + // Update each node's AccountId and add it to the new network map + for (const auto& node : getNodes()) + { + const std::string nodeAddress = node->getAddress().toString(); + const AccountId currentAccountId = node->getAccountId(); + + // Check if this node's full address is in the address book + auto it = addressToAccountId.find(nodeAddress); + if (it != addressToAccountId.end()) + { + const AccountId& newAccountId = it->second; + + if (!(currentAccountId == newAccountId)) + { + // Update the node's AccountId in place + node->setAccountId(newAccountId); + } + + // Add to new network map with updated AccountId + newNetworkMap[newAccountId].insert(node); + } + else + { + // Node not in address book, keep it with current AccountId + newNetworkMap[currentAccountId].insert(node); + } + } + + // Replace the internal network map + setNetworkInternal(newNetworkMap); + } + catch (const std::exception& e) + { + // Log and rethrow with a clearer message + throw std::runtime_error(std::string("Error updating node account IDs: ") + e.what()); + } + catch (...) + { + throw std::runtime_error("Unknown error updating node account IDs"); + } + + return *this; +} + //----- Network& Network::setLedgerId(const LedgerId& ledgerId) { @@ -129,14 +227,13 @@ Network& Network::setTransportSecurity(TLSBehavior tls) } //----- -std::vector Network::getNodeAccountIdsForExecute() +std::vector Network::getNodeAccountIdsForExecute(unsigned int maxAttempts) { std::unique_lock lock(*getLock()); - // Get either the 1/3 most healthy nodes, or the number of most healthy nodes specified by mMaxNodesPerRequest. - const std::vector> nodes = getNumberOfMostHealthyNodes( - mMaxNodesPerRequest > 0U ? std::min(mMaxNodesPerRequest, static_cast(getNodes().size())) - : static_cast(std::ceil(static_cast(getNodes().size()) / 3.0))); + // Get up to maxAttempts number of most healthy nodes + const std::vector> nodes = + getNumberOfMostHealthyNodes(std::min(maxAttempts, static_cast(getNodes().size()))); std::vector accountIds; accountIds.reserve(nodes.size()); @@ -163,7 +260,15 @@ std::unordered_map Network::getNetwork() const //----- Network::Network(const std::unordered_map& network) { - setNetwork(network); + // Map the network addresses for local development before setting the network + std::unordered_map mappedNetwork; + for (const auto& [address, accountId] : network) + { + const std::string mappedAddress = mapEndpointForLocalDevelopment(address); + mappedNetwork[mappedAddress] = accountId; + } + + setNetwork(mappedNetwork); } //----- diff --git a/src/sdk/tests/integration/AccountCreateTransactionIntegrationTests.cc b/src/sdk/tests/integration/AccountCreateTransactionIntegrationTests.cc index 0e931e2d..c213ad46 100644 --- a/src/sdk/tests/integration/AccountCreateTransactionIntegrationTests.cc +++ b/src/sdk/tests/integration/AccountCreateTransactionIntegrationTests.cc @@ -439,7 +439,6 @@ TEST_F(AccountCreateTransactionIntegrationTests, SerializeDeserializeEditCompare ASSERT_NO_THROW(createAccount = AccountCreateTransaction().setKeyWithoutAlias(testPublicKey)); const Hbar expectedBalance = Hbar(5LL); - std::vector nodeAccountIds = getTestClient().getClientNetwork()->getNodeAccountIdsForExecute(); // When std::vector transactionBytesSerialized; @@ -454,7 +453,6 @@ TEST_F(AccountCreateTransactionIntegrationTests, SerializeDeserializeEditCompare ASSERT_NO_THROW(txReceipt = createAccount.setInitialBalance(Hbar(5LL)) .setTransactionId(TransactionId::generate(getTestClient().getOperatorAccountId().value())) - .setNodeAccountIds(nodeAccountIds) // will fail if nodeAccountIds are bad so not checking equality .execute(getTestClient()) .getReceipt(getTestClient());); diff --git a/src/sdk/tests/integration/NodeUpdateTransactionIntegrationTests.cc b/src/sdk/tests/integration/NodeUpdateTransactionIntegrationTests.cc index d8127f1e..dadce166 100644 --- a/src/sdk/tests/integration/NodeUpdateTransactionIntegrationTests.cc +++ b/src/sdk/tests/integration/NodeUpdateTransactionIntegrationTests.cc @@ -1,14 +1,23 @@ // SPDX-License-Identifier: Apache-2.0 +#include "AccountCreateTransaction.h" +#include "AccountDeleteTransaction.h" +#include "AddressBookQuery.h" #include "BaseIntegrationTest.h" #include "ED25519PrivateKey.h" #include "FileId.h" #include "FreezeTransaction.h" +#include "Hbar.h" +#include "NodeAddress.h" +#include "NodeAddressBook.h" #include "NodeUpdateTransaction.h" #include "TransactionRecord.h" #include "TransactionResponse.h" +#include "exceptions/PrecheckStatusException.h" +#include "exceptions/ReceiptStatusException.h" #include "impl/HexConverter.h" #include +#include using namespace Hiero; @@ -29,6 +38,9 @@ class NodeUpdateTransactionIntegrationTests : public BaseIntegrationTest return internal::HexConverter::hexToBytes(mFileHash); } + // Node ID to update in tests + const uint64_t nodeIDToUpdate = 1; + private: const uint64_t mNodeId = 2; const AccountId mAccountId = AccountId::fromString("0.0.4"); @@ -104,4 +116,395 @@ TEST_F(NodeUpdateTransactionIntegrationTests, DISABLED_CanExecuteNodeUpdateTrans TransactionReceipt txReceipt; ASSERT_NO_THROW(txReceipt = txResponse.getReceipt(getTestClient())); +} + +//----- +TEST_F(NodeUpdateTransactionIntegrationTests, CanChangeNodeAccountIdToTheSameAccount) +{ + // Given + // Set up the network + std::unordered_map network; + network["localhost:51211"] = AccountId(4ULL); + + // Create client for the network + Client client = Client::forNetwork(network); + + // Set mirror network + std::vector mirrorNetwork = { "localhost:5600" }; + client.setMirrorNetwork(mirrorNetwork); + + // Set the operator to be account 0.0.2 + const std::string operatorKeyStr = + "302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137"; + std::shared_ptr originalOperatorKey = ED25519PrivateKey::fromString(operatorKeyStr); + client.setOperator(AccountId(2ULL), originalOperatorKey); + + const AccountId originalNodeAccountId = AccountId::fromString("0.0.4"); + + // When + TransactionResponse txResponse; + ASSERT_NO_THROW( + txResponse = NodeUpdateTransaction().setNodeId(nodeIDToUpdate).setAccountId(originalNodeAccountId).execute(client)); + + // Then + TransactionReceipt txReceipt; + ASSERT_NO_THROW(txReceipt = txResponse.setValidateStatus(true).getReceipt(client)); +} + +//----- +TEST_F(NodeUpdateTransactionIntegrationTests, ChangeNodeAccountIdMissingAdminSig) +{ + // Given - Set up the network + std::unordered_map network; + network["localhost:51211"] = AccountId(4ULL); + Client client = Client::forNetwork(network); + + std::vector mirrorNetwork = { "localhost:5600" }; + client.setMirrorNetwork(mirrorNetwork); + + const std::string operatorKeyStr = + "302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137"; + std::shared_ptr originalOperatorKey = ED25519PrivateKey::fromString(operatorKeyStr); + client.setOperator(AccountId(2ULL), originalOperatorKey); + + // Create new operator account + std::shared_ptr newOperatorKey = ED25519PrivateKey::generatePrivateKey(); + Hbar newBalance(2LL); + + TransactionResponse createResponse = + AccountCreateTransaction().setKey(newOperatorKey->getPublicKey()).setInitialBalance(newBalance).execute(client); + TransactionReceipt createReceipt = createResponse.setValidateStatus(true).getReceipt(client); + AccountId operatorAccountId = createReceipt.mAccountId.value(); + + client.setOperator(operatorAccountId, newOperatorKey); + + // When - Try to update without admin signature + TransactionResponse updateResponse = + NodeUpdateTransaction().setNodeId(nodeIDToUpdate).setAccountId(operatorAccountId).execute(client); + + // Then - Should fail with INVALID_SIGNATURE + EXPECT_THROW({ updateResponse.setValidateStatus(true).getReceipt(client); }, ReceiptStatusException); +} + +//----- +TEST_F(NodeUpdateTransactionIntegrationTests, ChangeNodeAccountIdMissingAccountSig) +{ + // Given - Set up the network + std::unordered_map network; + network["localhost:51211"] = AccountId(4ULL); + Client client = Client::forNetwork(network); + + std::vector mirrorNetwork = { "localhost:5600" }; + client.setMirrorNetwork(mirrorNetwork); + + const std::string operatorKeyStr = + "302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137"; + std::shared_ptr originalOperatorKey = ED25519PrivateKey::fromString(operatorKeyStr); + client.setOperator(AccountId(2ULL), originalOperatorKey); + + // Create new account + std::shared_ptr newOperatorKey = ED25519PrivateKey::generatePrivateKey(); + Hbar newBalance(2LL); + + TransactionResponse createResponse = + AccountCreateTransaction().setKey(newOperatorKey->getPublicKey()).setInitialBalance(newBalance).execute(client); + TransactionReceipt createReceipt = createResponse.setValidateStatus(true).getReceipt(client); + AccountId nodeAccountId = createReceipt.mAccountId.value(); + + // When - Try to update without new account signature + TransactionResponse updateResponse = + NodeUpdateTransaction().setNodeId(nodeIDToUpdate).setAccountId(nodeAccountId).execute(client); + + // Then - Should fail with INVALID_SIGNATURE + EXPECT_THROW({ updateResponse.setValidateStatus(true).getReceipt(client); }, ReceiptStatusException); +} + +//----- +TEST_F(NodeUpdateTransactionIntegrationTests, ChangeNodeAccountIdToNonExistentAccountId) +{ + // Given - Set up the network + std::unordered_map network; + network["localhost:51211"] = AccountId(4ULL); + Client client = Client::forNetwork(network); + + std::vector mirrorNetwork = { "localhost:5600" }; + client.setMirrorNetwork(mirrorNetwork); + + const std::string operatorKeyStr = + "302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137"; + std::shared_ptr originalOperatorKey = ED25519PrivateKey::fromString(operatorKeyStr); + client.setOperator(AccountId(2ULL), originalOperatorKey); + + // When - Try to update to non-existent account + TransactionResponse updateResponse = + NodeUpdateTransaction().setNodeId(nodeIDToUpdate).setAccountId(AccountId(9999999ULL)).execute(client); + + // Then - Should fail with INVALID_SIGNATURE + EXPECT_THROW({ updateResponse.setValidateStatus(true).getReceipt(client); }, ReceiptStatusException); +} + +//----- +TEST_F(NodeUpdateTransactionIntegrationTests, CanChangeNodeAccountIdToDeletedAccountId) +{ + // Given - Set up the network + std::unordered_map network; + network["localhost:51211"] = AccountId(4ULL); + Client client = Client::forNetwork(network); + + std::vector mirrorNetwork = { "localhost:5600" }; + client.setMirrorNetwork(mirrorNetwork); + + const std::string operatorKeyStr = + "302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137"; + std::shared_ptr originalOperatorKey = ED25519PrivateKey::fromString(operatorKeyStr); + client.setOperator(AccountId(2ULL), originalOperatorKey); + + // Create account to be deleted + std::shared_ptr newAccountKey = ED25519PrivateKey::generatePrivateKey(); + TransactionResponse createResponse = AccountCreateTransaction().setKey(newAccountKey->getPublicKey()).execute(client); + TransactionReceipt createReceipt = createResponse.setValidateStatus(true).getReceipt(client); + AccountId newAccount = createReceipt.mAccountId.value(); + + // Delete the account + AccountDeleteTransaction deleteTransaction = AccountDeleteTransaction() + .setDeleteAccountId(newAccount) + .setTransferAccountId(client.getOperatorAccountId().value()) + .freezeWith(&client); + TransactionResponse deleteResponse = deleteTransaction.sign(newAccountKey).execute(client); + deleteResponse.setValidateStatus(true).getReceipt(client); + + // When - Try to update to deleted account + NodeUpdateTransaction updateTransaction = + NodeUpdateTransaction().setNodeId(nodeIDToUpdate).setAccountId(newAccount).freezeWith(&client); + TransactionResponse updateResponse = updateTransaction.sign(newAccountKey).execute(client); + + // Then - Should fail with ACCOUNT_DELETED + EXPECT_THROW({ updateResponse.setValidateStatus(true).getReceipt(client); }, ReceiptStatusException); +} + +//----- +TEST_F(NodeUpdateTransactionIntegrationTests, ChangeNodeAccountIdNoBalance) +{ + // Given - Set up the network + std::unordered_map network; + network["localhost:51211"] = AccountId(4ULL); + Client client = Client::forNetwork(network); + + std::vector mirrorNetwork = { "localhost:5600" }; + client.setMirrorNetwork(mirrorNetwork); + + const std::string operatorKeyStr = + "302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137"; + std::shared_ptr originalOperatorKey = ED25519PrivateKey::fromString(operatorKeyStr); + client.setOperator(AccountId(2ULL), originalOperatorKey); + + // Create account with zero balance + std::shared_ptr newAccountKey = ED25519PrivateKey::generatePrivateKey(); + TransactionResponse createResponse = AccountCreateTransaction().setKey(newAccountKey->getPublicKey()).execute(client); + TransactionReceipt createReceipt = createResponse.setValidateStatus(true).getReceipt(client); + AccountId newAccount = createReceipt.mAccountId.value(); + + // When - Try to update to account with zero balance + NodeUpdateTransaction updateTransaction = + NodeUpdateTransaction().setNodeId(nodeIDToUpdate).setAccountId(newAccount).freezeWith(&client); + TransactionResponse updateResponse = updateTransaction.sign(newAccountKey).execute(client); + + // Then - Should fail with NODE_ACCOUNT_HAS_ZERO_BALANCE + EXPECT_THROW({ updateResponse.setValidateStatus(true).getReceipt(client); }, ReceiptStatusException); +} + +//----- +TEST_F(NodeUpdateTransactionIntegrationTests, CanChangeNodeAccountUpdateAddressbookAndRetry) +{ + // Given - Set up the network with two nodes + // Note: Use the actual DNS names that will appear in the address book + const AccountId originalNodeAccountId = AccountId(3ULL); + const AccountId node2OriginalAccountId = AccountId(4ULL); + + std::unordered_map network; + network["network-node1-svc.solo.svc.cluster.local:50211"] = originalNodeAccountId; + network["network-node2-svc.solo.svc.cluster.local:51211"] = node2OriginalAccountId; + + Client client = Client::forNetwork(network); + std::vector mirrorNetwork = { "127.0.0.1:5600" }; + client.setMirrorNetwork(mirrorNetwork); + + const std::string operatorKeyStr = + "302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137"; + std::shared_ptr originalOperatorKey = ED25519PrivateKey::fromString(operatorKeyStr); + client.setOperator(AccountId(2ULL), originalOperatorKey); + + NodeAddressBook addressBook = AddressBookQuery().setFileId(FileId::ADDRESS_BOOK).execute(client); + for (const auto& address : addressBook.getNodeAddresses()) + { + std::cout << address.toString() << std::endl; + } + + // Create the account that will be the new node account id + std::shared_ptr newAccountKey = ED25519PrivateKey::generatePrivateKey(); + AccountId newNodeAccountId; + std::cout << "Creating new node account" << std::endl; + ASSERT_NO_THROW(newNodeAccountId = AccountCreateTransaction() + .setKeyWithoutAlias(newAccountKey->getPublicKey()) + .setInitialBalance(Hbar(1LL)) + .execute(client) + .getReceipt(client) + .mAccountId.value()); + std::cout << "New node account created!" << std::endl; + + // Update node account id + TransactionReceipt txReceipt; + std::cout << "Updating node with new account ID" << std::endl; + ASSERT_NO_THROW(txReceipt = NodeUpdateTransaction() + .setNodeId(nodeIDToUpdate) + .setAccountId(newNodeAccountId) + .freezeWith(&client) + .sign(newAccountKey) + .execute(client) + .getReceipt(client)); + std::cout << "Node updated!" << std::endl; + + // Poll the address book until we see the updated account ID, or timeout after 2 minutes + std::cout << "Waiting for mirror node to update..." << std::endl; + const auto startTime = std::chrono::steady_clock::now(); + const auto timeout = std::chrono::seconds(120); + bool addressBookUpdated = false; + + while (std::chrono::steady_clock::now() - startTime < timeout) + { + std::this_thread::sleep_for(std::chrono::seconds(3)); + + try + { + NodeAddressBook currentAddressBook = AddressBookQuery().setFileId(FileId::ADDRESS_BOOK).execute(client); + for (const auto& address : currentAddressBook.getNodeAddresses()) + { + if (address.getNodeId() == nodeIDToUpdate && address.getAccountId() == newNodeAccountId) + { + std::cout << "Mirror node updated! Node " << nodeIDToUpdate << " now has AccountId " + << newNodeAccountId.toString() << std::endl; + addressBookUpdated = true; + break; + } + } + + if (addressBookUpdated) + { + break; + } + } + catch (...) + { + // Ignore query errors and keep polling + } + } + + if (!addressBookUpdated) + { + std::cout << "WARNING: Mirror node did not update within timeout period" << std::endl; + } + + // Submit a transaction targeting ONLY node 0.0.4 (which is now invalid) + // This should trigger INVALID_NODE_ACCOUNT, update the address book to learn that node2 is now different + std::shared_ptr key = ED25519PrivateKey::generatePrivateKey(); + + // Expected throw: node 0.0.4 is now invalid, address book should be updated + std::cout << "Creating new account targeting invalid node" << std::endl; + EXPECT_THROW(const TransactionResponse txResponse = AccountCreateTransaction() + .setKeyWithoutAlias(key->getPublicKey()) + .setNodeAccountIds({ node2OriginalAccountId }) + .execute(client), + PrecheckStatusException); + std::cout << "New account successfully not created!" << std::endl; + + std::cout << "Creating new account with address book retry" << std::endl; + EXPECT_NO_THROW(txReceipt = AccountCreateTransaction() + .setKeyWithoutAlias(key->getPublicKey()) + .setNodeAccountIds({ newNodeAccountId }) + .execute(client) + .getReceipt(client)); + std::cout << "New account created!" << std::endl; + + // Revert the node account id so other tests can still function properly. + std::cout << "Resetting node account ID" << std::endl; + ASSERT_NO_THROW(txReceipt = NodeUpdateTransaction() + .setNodeId(nodeIDToUpdate) + .setAccountId(node2OriginalAccountId) + .execute(client) + .getReceipt(client)); + std::cout << "Node account ID reset!" << std::endl; + + // Wait for mirror node to import data AND for consensus nodes to synchronize + std::cout << "Waiting for mirror node to update..." << std::endl; + std::this_thread::sleep_for(std::chrono::seconds(5)); + std::cout << "Mirror node should be good!" << std::endl; +} + +//----- +TEST_F(NodeUpdateTransactionIntegrationTests, CanChangeNodeAccountWithoutMirrorNodeSetup) +{ + // Given - Set up the network without mirror node + // Note: Use the actual DNS names that will appear in the address book + const AccountId originalNodeAccountId = AccountId(3ULL); + const AccountId node2OriginalAccountId = AccountId(4ULL); + + std::unordered_map network; + network["network-node1-svc.solo.svc.cluster.local:50211"] = originalNodeAccountId; + network["network-node2-svc.solo.svc.cluster.local:51211"] = node2OriginalAccountId; + + Client client = Client::forNetwork(network); + // Note: No mirror network set + + const std::string operatorKeyStr = + "302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137"; + std::shared_ptr originalOperatorKey = ED25519PrivateKey::fromString(operatorKeyStr); + client.setOperator(AccountId(2ULL), originalOperatorKey); + + // Create the account that will be the new node account id + std::shared_ptr newAccountKey = ED25519PrivateKey::generatePrivateKey(); + AccountId newNodeAccountId; + std::cout << "Creating new node account" << std::endl; + ASSERT_NO_THROW(newNodeAccountId = AccountCreateTransaction() + .setKeyWithoutAlias(newAccountKey->getPublicKey()) + .setInitialBalance(Hbar(1LL)) + .execute(client) + .getReceipt(client) + .mAccountId.value()); + std::cout << "New node account created!" << std::endl; + + // Update node account id + TransactionReceipt txReceipt; + std::cout << "Updating node with new account ID" << std::endl; + ASSERT_NO_THROW(txReceipt = NodeUpdateTransaction() + .setNodeId(nodeIDToUpdate) + .setAccountId(newNodeAccountId) + .freezeWith(&client) + .sign(newAccountKey) + .execute(client) + .getReceipt(client)); + std::cout << "Node updated!" << std::endl; + + // Submit to the updated node - should retry + std::shared_ptr key = ED25519PrivateKey::generatePrivateKey(); + std::cout << "Creating new account" << std::endl; + EXPECT_NO_THROW(txReceipt = AccountCreateTransaction() + .setKeyWithoutAlias(key) + .setNodeAccountIds({ originalNodeAccountId, node2OriginalAccountId }) + .execute(client) + .getReceipt(client)); + std::cout << "New account created!" << std::endl; + + // Revert the node account id so other tests can still function properly. + std::cout << "Resetting node account ID" << std::endl; + ASSERT_NO_THROW(txReceipt = NodeUpdateTransaction() + .setNodeId(nodeIDToUpdate) + .setAccountId(node2OriginalAccountId) + .execute(client) + .getReceipt(client)); + std::cout << "Node account ID reset!" << std::endl; + + // Wait for mirror node to import data AND for consensus nodes to synchronize + std::cout << "Waiting for mirror node to update..." << std::endl; + std::this_thread::sleep_for(std::chrono::seconds(5)); + std::cout << "Mirror node should be good!" << std::endl; } \ No newline at end of file diff --git a/src/sdk/tests/unit/HbarTransferUnitTests.cc b/src/sdk/tests/unit/HbarTransferUnitTests.cc index 03bef689..28163a60 100644 --- a/src/sdk/tests/unit/HbarTransferUnitTests.cc +++ b/src/sdk/tests/unit/HbarTransferUnitTests.cc @@ -3,8 +3,8 @@ #include "Hbar.h" #include "HbarTransfer.h" -#include #include +#include using namespace Hiero; diff --git a/src/sdk/tests/unit/NetworkUnitTests.cc b/src/sdk/tests/unit/NetworkUnitTests.cc index 46e5b2a7..f346c97d 100644 --- a/src/sdk/tests/unit/NetworkUnitTests.cc +++ b/src/sdk/tests/unit/NetworkUnitTests.cc @@ -24,7 +24,7 @@ TEST_F(NetworkUnitTests, ConstructForMainnet) std::vector nodeAccountIds; EXPECT_NO_THROW(networkMap = mainnetNetwork.getNetwork()); - EXPECT_NO_THROW(nodeAccountIds = mainnetNetwork.getNodeAccountIdsForExecute()); + EXPECT_NO_THROW(nodeAccountIds = mainnetNetwork.getNodeAccountIdsForExecute(DEFAULT_MAX_ATTEMPTS)); EXPECT_GT(networkMap.size(), 0); EXPECT_GT(nodeAccountIds.size(), 0); @@ -43,7 +43,7 @@ TEST_F(NetworkUnitTests, ConstructForTestnet) std::vector nodeAccountIds; EXPECT_NO_THROW(networkMap = testnetNetwork.getNetwork()); - EXPECT_NO_THROW(nodeAccountIds = testnetNetwork.getNodeAccountIdsForExecute()); + EXPECT_NO_THROW(nodeAccountIds = testnetNetwork.getNodeAccountIdsForExecute(DEFAULT_MAX_ATTEMPTS)); EXPECT_GT(networkMap.size(), 0); EXPECT_GT(nodeAccountIds.size(), 0); @@ -62,7 +62,7 @@ TEST_F(NetworkUnitTests, ConstructForPreviewnet) std::vector nodeAccountIds; EXPECT_NO_THROW(networkMap = previewnetNetwork.getNetwork()); - EXPECT_NO_THROW(nodeAccountIds = previewnetNetwork.getNodeAccountIdsForExecute()); + EXPECT_NO_THROW(nodeAccountIds = previewnetNetwork.getNodeAccountIdsForExecute(DEFAULT_MAX_ATTEMPTS)); EXPECT_GT(networkMap.size(), 0); EXPECT_GT(nodeAccountIds.size(), 0); @@ -87,7 +87,7 @@ TEST_F(NetworkUnitTests, ConstructCustomNetwork) std::vector nodeAccountIds; EXPECT_NO_THROW(networkMap = customNetwork.getNetwork()); - EXPECT_NO_THROW(nodeAccountIds = customNetwork.getNodeAccountIdsForExecute()); + EXPECT_NO_THROW(nodeAccountIds = customNetwork.getNodeAccountIdsForExecute(DEFAULT_MAX_ATTEMPTS)); EXPECT_GT(networkMap.size(), 0); EXPECT_GT(nodeAccountIds.size(), 0);