Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/sdk/main/include/Client.h
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,24 @@ class Client
*/
[[nodiscard]] bool isAutoValidateChecksumsEnabled() const;

/**
* Set whether receipt and record queries may fail over to other nodes when the submitting node is unavailable.
* When enabled, queries still start with the submitting node and advance
* to other eligible nodes in deterministic order only on failure. When
* disabled (default), strict single-node pinning is preserved.
*
* @param allow \c true to allow receipt/record query failover to other nodes.
* @return A reference to this Client with the newly-set failover policy.
*/
Client& setAllowReceiptNodeFailover(bool allow);

/**
* Is receipt/record query node failover allowed for this Client?
*
* @return \c TRUE if node failover is allowed for receipt/record queries, otherwise \c FALSE.
*/
[[nodiscard]] bool getAllowReceiptNodeFailover() const;

/**
* Replace the network being used by this Client with nodes contained in an address book.
*
Expand Down
34 changes: 31 additions & 3 deletions src/sdk/main/include/TransactionResponse.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,12 @@ class TransactionResponse
* @param nodeId The ID of the node account to which this TransactionResponse's corresponding Transaction was sent.
* @param transactionId The ID of this TransactionResponse's corresponding Transaction.
* @param hash The hash of this TransactionResponse's corresponding Transaction.
* @param transactionNodeAccountIds The list of node account IDs configured on the transaction at execution time.
*/
TransactionResponse(AccountId nodeId, TransactionId transactionId, std::vector<std::byte> hash);
TransactionResponse(AccountId nodeId,
TransactionId transactionId,
std::vector<std::byte> hash,
std::vector<AccountId> transactionNodeAccountIds = {});

/**
* Get a TransactionReceipt for this TransactionResponse's corresponding Transaction.
Expand Down Expand Up @@ -74,10 +78,16 @@ class TransactionResponse

/**
* Construct a TransactionReceiptQuery for this TransactionResponse's corresponding Transaction.
* When a non-null Client is provided and its failover flag is
* enabled, the query will target the submitting node first, then
* other eligible nodes in deterministic order.
*
* @param client Optional pointer to the Client. When null or
* failover is disabled, the query is pinned to the submitting
* node only.
* @return The constructed TransactionReceiptQuery.
*/
[[nodiscard]] TransactionReceiptQuery getReceiptQuery() const;
[[nodiscard]] TransactionReceiptQuery getReceiptQuery(const Client* client = nullptr) const;

/**
* Get a TransactionReceipt for this TransactionResponse's corresponding Transaction asynchronously.
Expand Down Expand Up @@ -214,10 +224,14 @@ class TransactionResponse

/**
* Construct a TransactionRecordQuery for this TransactionResponse's corresponding Transaction.
* When a non-null Client is provided and its failover flag is enabled, the query will target
* the submitting node first, then other eligible nodes in deterministic order.
*
* @param client Optional pointer to the Client. When null or failover is disabled, the query is
* pinned to the submitting node only.
* @return The constructed TransactionRecordQuery.
*/
[[nodiscard]] TransactionRecordQuery getRecordQuery() const;
[[nodiscard]] TransactionRecordQuery getRecordQuery(const Client* client = nullptr) const;

/**
* Get a TransactionRecord for this TransactionResponse's corresponding Transaction asynchronously.
Expand Down Expand Up @@ -374,6 +388,20 @@ class TransactionResponse
TransactionId mTransactionId;

private:
/**
* Build a deduplicated, deterministically sorted failover node list starting with the submitting
* node, using either the transaction's own node IDs or the client's network nodes as candidates.
*
* @param client The Client whose network is used when transaction node IDs are unavailable.
* @return An ordered vector of AccountId values for query node targeting.
*/
[[nodiscard]] std::vector<AccountId> buildFailoverNodeList(const Client& client) const;

/**
* The node account IDs that were configured on the transaction during execution.
*/
std::vector<AccountId> mTransactionNodeAccountIds;

/**
* Did this TransactionResponse's corresponding Transaction have a successful pre-check?
*/
Expand Down
20 changes: 20 additions & 0 deletions src/sdk/main/src/Client.cc
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ struct Client::ClientImpl
// Should this Client automatically validate entity checksums?
bool mAutoValidateChecksums = false;

// Should this Client allow receipt/record queries to fail over to other nodes when the submitting
// node is unavailable? When true, queries start with the submitting node and advance to others on
// failure. Defaults to false (strict single-node pinning).
bool mAllowReceiptNodeFailover = false;

// Has this Client made its initial network update? This is utilized in case
// the user updates the network update period before the initial update is
// made, as it prevents the update period from being overwritten.
Expand Down Expand Up @@ -671,6 +676,21 @@ bool Client::isAutoValidateChecksumsEnabled() const
return mImpl->mAutoValidateChecksums;
}

//-----
Client& Client::setAllowReceiptNodeFailover(bool allow)
{
std::unique_lock lock(mImpl->mMutex);
mImpl->mAllowReceiptNodeFailover = allow;
return *this;
}

//-----
bool Client::getAllowReceiptNodeFailover() const
{
std::unique_lock lock(mImpl->mMutex);
return mImpl->mAllowReceiptNodeFailover;
}

//-----
Client& Client::setNetworkFromAddressBook(const NodeAddressBook& addressBook)
{
Expand Down
18 changes: 9 additions & 9 deletions src/sdk/main/src/Transaction.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1421,15 +1421,15 @@ TransactionId Transaction<SdkRequestType>::getCurrentTransactionId() const
template<typename SdkRequestType>
TransactionResponse Transaction<SdkRequestType>::mapResponse(const proto::TransactionResponse&) const
{
return TransactionResponse(
Executable<SdkRequestType, proto::Transaction, proto::TransactionResponse, TransactionResponse>::getNodeAccountIds()
.at(mImpl->mTransactionIndex %
Executable<SdkRequestType, proto::Transaction, proto::TransactionResponse, TransactionResponse>::
getNodeAccountIds()
.size()),
getCurrentTransactionId(),
internal::OpenSSLUtils::computeSHA384(internal::Utilities::stringToByteVector(
getTransactionProtobufObject(mImpl->mTransactionIndex).signedtransactionbytes())));
const auto& nodeAccountIds =
Executable<SdkRequestType, proto::Transaction, proto::TransactionResponse, TransactionResponse>::
getNodeAccountIds();

return TransactionResponse(nodeAccountIds.at(mImpl->mTransactionIndex % nodeAccountIds.size()),
getCurrentTransactionId(),
internal::OpenSSLUtils::computeSHA384(internal::Utilities::stringToByteVector(
getTransactionProtobufObject(mImpl->mTransactionIndex).signedtransactionbytes())),
nodeAccountIds);
}

//-----
Expand Down
96 changes: 89 additions & 7 deletions src/sdk/main/src/TransactionResponse.cc
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,20 @@

#include <nlohmann/json.hpp>

#include <algorithm>
#include <unordered_set>

namespace Hiero
{
//-----
TransactionResponse::TransactionResponse(AccountId nodeId, TransactionId transactionId, std::vector<std::byte> hash)
TransactionResponse::TransactionResponse(AccountId nodeId,
TransactionId transactionId,
std::vector<std::byte> hash,
std::vector<AccountId> transactionNodeAccountIds)
: mNodeId(std::move(nodeId))
, mTransactionHash(std::move(hash))
, mTransactionId(std::move(transactionId))
, mTransactionNodeAccountIds(std::move(transactionNodeAccountIds))
{
}

Expand All @@ -29,7 +36,7 @@ TransactionReceipt TransactionResponse::getReceipt(const Client& client) const
TransactionReceipt TransactionResponse::getReceipt(const Client& client,
const std::chrono::system_clock::duration& timeout) const
{
TransactionReceipt txReceipt = getReceiptQuery().execute(client, timeout);
TransactionReceipt txReceipt = getReceiptQuery(&client).execute(client, timeout);

if (mValidateStatus)
{
Expand All @@ -40,9 +47,20 @@ TransactionReceipt TransactionResponse::getReceipt(const Client& client,
}

//-----
TransactionReceiptQuery TransactionResponse::getReceiptQuery() const
TransactionReceiptQuery TransactionResponse::getReceiptQuery(const Client* client) const
{
return TransactionReceiptQuery().setTransactionId(mTransactionId).setNodeAccountIds({ mNodeId });
auto query = TransactionReceiptQuery().setTransactionId(mTransactionId);

if (client && client->getAllowReceiptNodeFailover())
{
query.setNodeAccountIds(buildFailoverNodeList(*client));
}
else
{
query.setNodeAccountIds({ mNodeId });
}

return query;
}

//-----
Expand Down Expand Up @@ -123,13 +141,77 @@ TransactionRecord TransactionResponse::getRecord(const Client& client) const
TransactionRecord TransactionResponse::getRecord(const Client& client,
const std::chrono::system_clock::duration& timeout) const
{
return TransactionRecordQuery().setTransactionId(mTransactionId).execute(client, timeout);
return getRecordQuery(&client).execute(client, timeout);
}

//-----
TransactionRecordQuery TransactionResponse::getRecordQuery() const
TransactionRecordQuery TransactionResponse::getRecordQuery(const Client* client) const
{
return TransactionRecordQuery().setTransactionId(mTransactionId).setNodeAccountIds({ mNodeId });
auto query = TransactionRecordQuery().setTransactionId(mTransactionId);

if (client && client->getAllowReceiptNodeFailover())
{
query.setNodeAccountIds(buildFailoverNodeList(*client));
}
else
{
query.setNodeAccountIds({ mNodeId });
}

return query;
}

//-----
std::vector<AccountId> TransactionResponse::buildFailoverNodeList(const Client& client) const
{
std::vector<AccountId> candidates;
if (!mTransactionNodeAccountIds.empty())
{
candidates = mTransactionNodeAccountIds;
}
else
{
std::unordered_set<std::string> seen;
for (const auto& [url, accountId] : client.getNetwork())
{
const std::string key = accountId.toString();
if (seen.find(key) == seen.end())
{
seen.insert(key);
candidates.push_back(accountId);
}
}
}

std::unordered_set<std::string> seen;
std::vector<AccountId> nodeIds;
nodeIds.push_back(mNodeId);
seen.insert(mNodeId.toString());

std::vector<AccountId> others;
for (const auto& node : candidates)
{
const std::string key = node.toString();
if (seen.find(key) == seen.end())
{
seen.insert(key);
others.push_back(node);
}
}

std::sort(others.begin(),
others.end(),
[](const AccountId& a, const AccountId& b)
{
if (a.mShardNum != b.mShardNum)
return a.mShardNum < b.mShardNum;
if (a.mRealmNum != b.mRealmNum)
return a.mRealmNum < b.mRealmNum;
return a.mAccountNum.value_or(0ULL) < b.mAccountNum.value_or(0ULL);
});

nodeIds.insert(nodeIds.end(), others.begin(), others.end());
return nodeIds;
}

//-----
Expand Down
20 changes: 19 additions & 1 deletion src/sdk/tests/unit/ClientUnitTests.cc
Original file line number Diff line number Diff line change
Expand Up @@ -236,4 +236,22 @@ TEST_F(ClientUnitTests, DefaultValuesRespectConstraint)
// Setting them explicitly with defaults should work
EXPECT_NO_THROW(client.setGrpcDeadline(DEFAULT_GRPC_DEADLINE));
EXPECT_NO_THROW(client.setRequestTimeout(DEFAULT_REQUEST_TIMEOUT));
}
}

//-----
TEST_F(ClientUnitTests, AllowReceiptNodeFailoverDefaultIsFalse)
{
Client client;
EXPECT_FALSE(client.getAllowReceiptNodeFailover());
}

//-----
TEST_F(ClientUnitTests, SetAllowReceiptNodeFailover)
{
Client client;
client.setAllowReceiptNodeFailover(true);
EXPECT_TRUE(client.getAllowReceiptNodeFailover());

client.setAllowReceiptNodeFailover(false);
EXPECT_FALSE(client.getAllowReceiptNodeFailover());
}
51 changes: 51 additions & 0 deletions src/sdk/tests/unit/TransactionResponseUnitTests.cc
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
// SPDX-License-Identifier: Apache-2.0
#include "AccountId.h"
#include "Client.h"
#include "TransactionReceiptQuery.h"
#include "TransactionRecordQuery.h"
#include "TransactionResponse.h"

#include <gtest/gtest.h>
Expand Down Expand Up @@ -27,3 +31,50 @@ TEST_F(TransactionResponseUnitTests, ContructTransactionResponse)
EXPECT_EQ(transactionResponse.mTransactionHash, hash);
EXPECT_EQ(transactionResponse.mTransactionId, transactionId);
}

//-----
TEST_F(TransactionResponseUnitTests, GetReceiptQueryNullClientPinsToSubmittingNode)
{
// Given
const AccountId submittingNode(0ULL, 0ULL, 3ULL);
const TransactionId txId = TransactionId::withValidStart(AccountId(1ULL), std::chrono::system_clock::now());
const TransactionResponse response(submittingNode, txId, {}, { submittingNode, AccountId(0ULL, 0ULL, 4ULL) });

// When / Then: no client → pinned to submitting node only
EXPECT_EQ(response.getReceiptQuery(nullptr).getNodeAccountIds(), (std::vector<AccountId>{ submittingNode }));
}

//-----
TEST_F(TransactionResponseUnitTests, GetReceiptQueryFailoverBuildsNodeListFromTransactionNodes)
{
// Given: transaction nodes 3, 5, 4 (unsorted); submitting node is 3
const AccountId submittingNode(0ULL, 0ULL, 3ULL);
const AccountId node4(0ULL, 0ULL, 4ULL);
const AccountId node5(0ULL, 0ULL, 5ULL);
const TransactionId txId = TransactionId::withValidStart(AccountId(1ULL), std::chrono::system_clock::now());
const TransactionResponse response(submittingNode, txId, {}, { submittingNode, node5, node4 });

Client client;
client.setAllowReceiptNodeFailover(true);

// When / Then: submitting node first then sorting remaining by acc. number
const std::vector<AccountId> expected = { submittingNode, node4, node5 };
EXPECT_EQ(response.getReceiptQuery(&client).getNodeAccountIds(), expected);
}

//-----
TEST_F(TransactionResponseUnitTests, SingleFlagGovernsReceiptAndRecordQueryNodeList)
{
// Given
const AccountId submittingNode(0ULL, 0ULL, 3ULL);
const AccountId node4(0ULL, 0ULL, 4ULL);
const TransactionId txId = TransactionId::withValidStart(AccountId(1ULL), std::chrono::system_clock::now());
const TransactionResponse response(submittingNode, txId, {}, { submittingNode, node4 });

Client client;
client.setAllowReceiptNodeFailover(true);

// When / Then: same flag, same node list for both query types
EXPECT_EQ(response.getReceiptQuery(&client).getNodeAccountIds(),
response.getRecordQuery(&client).getNodeAccountIds());
}
Loading