Skip to content

Commit 868ab27

Browse files
authored
feat: implement opt-in receipt and record query failover (#1220)
Signed-off-by: Aditya Arya <arya050411@gmail.com>
1 parent b0a33f3 commit 868ab27

File tree

7 files changed

+237
-20
lines changed

7 files changed

+237
-20
lines changed

src/sdk/main/include/Client.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,24 @@ class Client
349349
*/
350350
[[nodiscard]] bool isAutoValidateChecksumsEnabled() const;
351351

352+
/**
353+
* Set whether receipt and record queries may fail over to other nodes when the submitting node is unavailable.
354+
* When enabled, queries still start with the submitting node and advance
355+
* to other eligible nodes in deterministic order only on failure. When
356+
* disabled (default), strict single-node pinning is preserved.
357+
*
358+
* @param allow \c true to allow receipt/record query failover to other nodes.
359+
* @return A reference to this Client with the newly-set failover policy.
360+
*/
361+
Client& setAllowReceiptNodeFailover(bool allow);
362+
363+
/**
364+
* Is receipt/record query node failover allowed for this Client?
365+
*
366+
* @return \c TRUE if node failover is allowed for receipt/record queries, otherwise \c FALSE.
367+
*/
368+
[[nodiscard]] bool getAllowReceiptNodeFailover() const;
369+
352370
/**
353371
* Replace the network being used by this Client with nodes contained in an address book.
354372
*

src/sdk/main/include/TransactionResponse.h

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,12 @@ class TransactionResponse
4040
* @param nodeId The ID of the node account to which this TransactionResponse's corresponding Transaction was sent.
4141
* @param transactionId The ID of this TransactionResponse's corresponding Transaction.
4242
* @param hash The hash of this TransactionResponse's corresponding Transaction.
43+
* @param transactionNodeAccountIds The list of node account IDs configured on the transaction at execution time.
4344
*/
44-
TransactionResponse(AccountId nodeId, TransactionId transactionId, std::vector<std::byte> hash);
45+
TransactionResponse(AccountId nodeId,
46+
TransactionId transactionId,
47+
std::vector<std::byte> hash,
48+
std::vector<AccountId> transactionNodeAccountIds = {});
4549

4650
/**
4751
* Get a TransactionReceipt for this TransactionResponse's corresponding Transaction.
@@ -74,10 +78,16 @@ class TransactionResponse
7478

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

8292
/**
8393
* Get a TransactionReceipt for this TransactionResponse's corresponding Transaction asynchronously.
@@ -214,10 +224,14 @@ class TransactionResponse
214224

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

222236
/**
223237
* Get a TransactionRecord for this TransactionResponse's corresponding Transaction asynchronously.
@@ -374,6 +388,20 @@ class TransactionResponse
374388
TransactionId mTransactionId;
375389

376390
private:
391+
/**
392+
* Build a deduplicated, deterministically sorted failover node list starting with the submitting
393+
* node, using either the transaction's own node IDs or the client's network nodes as candidates.
394+
*
395+
* @param client The Client whose network is used when transaction node IDs are unavailable.
396+
* @return An ordered vector of AccountId values for query node targeting.
397+
*/
398+
[[nodiscard]] std::vector<AccountId> buildFailoverNodeList(const Client& client) const;
399+
400+
/**
401+
* The node account IDs that were configured on the transaction during execution.
402+
*/
403+
std::vector<AccountId> mTransactionNodeAccountIds;
404+
377405
/**
378406
* Did this TransactionResponse's corresponding Transaction have a successful pre-check?
379407
*/

src/sdk/main/src/Client.cc

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,11 @@ struct Client::ClientImpl
9696
// Should this Client automatically validate entity checksums?
9797
bool mAutoValidateChecksums = false;
9898

99+
// Should this Client allow receipt/record queries to fail over to other nodes when the submitting
100+
// node is unavailable? When true, queries start with the submitting node and advance to others on
101+
// failure. Defaults to false (strict single-node pinning).
102+
bool mAllowReceiptNodeFailover = false;
103+
99104
// Has this Client made its initial network update? This is utilized in case
100105
// the user updates the network update period before the initial update is
101106
// made, as it prevents the update period from being overwritten.
@@ -671,6 +676,21 @@ bool Client::isAutoValidateChecksumsEnabled() const
671676
return mImpl->mAutoValidateChecksums;
672677
}
673678

679+
//-----
680+
Client& Client::setAllowReceiptNodeFailover(bool allow)
681+
{
682+
std::unique_lock lock(mImpl->mMutex);
683+
mImpl->mAllowReceiptNodeFailover = allow;
684+
return *this;
685+
}
686+
687+
//-----
688+
bool Client::getAllowReceiptNodeFailover() const
689+
{
690+
std::unique_lock lock(mImpl->mMutex);
691+
return mImpl->mAllowReceiptNodeFailover;
692+
}
693+
674694
//-----
675695
Client& Client::setNetworkFromAddressBook(const NodeAddressBook& addressBook)
676696
{

src/sdk/main/src/Transaction.cc

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1421,15 +1421,15 @@ TransactionId Transaction<SdkRequestType>::getCurrentTransactionId() const
14211421
template<typename SdkRequestType>
14221422
TransactionResponse Transaction<SdkRequestType>::mapResponse(const proto::TransactionResponse&) const
14231423
{
1424-
return TransactionResponse(
1425-
Executable<SdkRequestType, proto::Transaction, proto::TransactionResponse, TransactionResponse>::getNodeAccountIds()
1426-
.at(mImpl->mTransactionIndex %
1427-
Executable<SdkRequestType, proto::Transaction, proto::TransactionResponse, TransactionResponse>::
1428-
getNodeAccountIds()
1429-
.size()),
1430-
getCurrentTransactionId(),
1431-
internal::OpenSSLUtils::computeSHA384(internal::Utilities::stringToByteVector(
1432-
getTransactionProtobufObject(mImpl->mTransactionIndex).signedtransactionbytes())));
1424+
const auto& nodeAccountIds =
1425+
Executable<SdkRequestType, proto::Transaction, proto::TransactionResponse, TransactionResponse>::
1426+
getNodeAccountIds();
1427+
1428+
return TransactionResponse(nodeAccountIds.at(mImpl->mTransactionIndex % nodeAccountIds.size()),
1429+
getCurrentTransactionId(),
1430+
internal::OpenSSLUtils::computeSHA384(internal::Utilities::stringToByteVector(
1431+
getTransactionProtobufObject(mImpl->mTransactionIndex).signedtransactionbytes())),
1432+
nodeAccountIds);
14331433
}
14341434

14351435
//-----

src/sdk/main/src/TransactionResponse.cc

Lines changed: 89 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,20 @@
99

1010
#include <nlohmann/json.hpp>
1111

12+
#include <algorithm>
13+
#include <unordered_set>
14+
1215
namespace Hiero
1316
{
1417
//-----
15-
TransactionResponse::TransactionResponse(AccountId nodeId, TransactionId transactionId, std::vector<std::byte> hash)
18+
TransactionResponse::TransactionResponse(AccountId nodeId,
19+
TransactionId transactionId,
20+
std::vector<std::byte> hash,
21+
std::vector<AccountId> transactionNodeAccountIds)
1622
: mNodeId(std::move(nodeId))
1723
, mTransactionHash(std::move(hash))
1824
, mTransactionId(std::move(transactionId))
25+
, mTransactionNodeAccountIds(std::move(transactionNodeAccountIds))
1926
{
2027
}
2128

@@ -29,7 +36,7 @@ TransactionReceipt TransactionResponse::getReceipt(const Client& client) const
2936
TransactionReceipt TransactionResponse::getReceipt(const Client& client,
3037
const std::chrono::system_clock::duration& timeout) const
3138
{
32-
TransactionReceipt txReceipt = getReceiptQuery().execute(client, timeout);
39+
TransactionReceipt txReceipt = getReceiptQuery(&client).execute(client, timeout);
3340

3441
if (mValidateStatus)
3542
{
@@ -40,9 +47,20 @@ TransactionReceipt TransactionResponse::getReceipt(const Client& client,
4047
}
4148

4249
//-----
43-
TransactionReceiptQuery TransactionResponse::getReceiptQuery() const
50+
TransactionReceiptQuery TransactionResponse::getReceiptQuery(const Client* client) const
4451
{
45-
return TransactionReceiptQuery().setTransactionId(mTransactionId).setNodeAccountIds({ mNodeId });
52+
auto query = TransactionReceiptQuery().setTransactionId(mTransactionId);
53+
54+
if (client && client->getAllowReceiptNodeFailover())
55+
{
56+
query.setNodeAccountIds(buildFailoverNodeList(*client));
57+
}
58+
else
59+
{
60+
query.setNodeAccountIds({ mNodeId });
61+
}
62+
63+
return query;
4664
}
4765

4866
//-----
@@ -123,13 +141,77 @@ TransactionRecord TransactionResponse::getRecord(const Client& client) const
123141
TransactionRecord TransactionResponse::getRecord(const Client& client,
124142
const std::chrono::system_clock::duration& timeout) const
125143
{
126-
return TransactionRecordQuery().setTransactionId(mTransactionId).execute(client, timeout);
144+
return getRecordQuery(&client).execute(client, timeout);
127145
}
128146

129147
//-----
130-
TransactionRecordQuery TransactionResponse::getRecordQuery() const
148+
TransactionRecordQuery TransactionResponse::getRecordQuery(const Client* client) const
131149
{
132-
return TransactionRecordQuery().setTransactionId(mTransactionId).setNodeAccountIds({ mNodeId });
150+
auto query = TransactionRecordQuery().setTransactionId(mTransactionId);
151+
152+
if (client && client->getAllowReceiptNodeFailover())
153+
{
154+
query.setNodeAccountIds(buildFailoverNodeList(*client));
155+
}
156+
else
157+
{
158+
query.setNodeAccountIds({ mNodeId });
159+
}
160+
161+
return query;
162+
}
163+
164+
//-----
165+
std::vector<AccountId> TransactionResponse::buildFailoverNodeList(const Client& client) const
166+
{
167+
std::vector<AccountId> candidates;
168+
if (!mTransactionNodeAccountIds.empty())
169+
{
170+
candidates = mTransactionNodeAccountIds;
171+
}
172+
else
173+
{
174+
std::unordered_set<std::string> seen;
175+
for (const auto& [url, accountId] : client.getNetwork())
176+
{
177+
const std::string key = accountId.toString();
178+
if (seen.find(key) == seen.end())
179+
{
180+
seen.insert(key);
181+
candidates.push_back(accountId);
182+
}
183+
}
184+
}
185+
186+
std::unordered_set<std::string> seen;
187+
std::vector<AccountId> nodeIds;
188+
nodeIds.push_back(mNodeId);
189+
seen.insert(mNodeId.toString());
190+
191+
std::vector<AccountId> others;
192+
for (const auto& node : candidates)
193+
{
194+
const std::string key = node.toString();
195+
if (seen.find(key) == seen.end())
196+
{
197+
seen.insert(key);
198+
others.push_back(node);
199+
}
200+
}
201+
202+
std::sort(others.begin(),
203+
others.end(),
204+
[](const AccountId& a, const AccountId& b)
205+
{
206+
if (a.mShardNum != b.mShardNum)
207+
return a.mShardNum < b.mShardNum;
208+
if (a.mRealmNum != b.mRealmNum)
209+
return a.mRealmNum < b.mRealmNum;
210+
return a.mAccountNum.value_or(0ULL) < b.mAccountNum.value_or(0ULL);
211+
});
212+
213+
nodeIds.insert(nodeIds.end(), others.begin(), others.end());
214+
return nodeIds;
133215
}
134216

135217
//-----

src/sdk/tests/unit/ClientUnitTests.cc

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,4 +236,22 @@ TEST_F(ClientUnitTests, DefaultValuesRespectConstraint)
236236
// Setting them explicitly with defaults should work
237237
EXPECT_NO_THROW(client.setGrpcDeadline(DEFAULT_GRPC_DEADLINE));
238238
EXPECT_NO_THROW(client.setRequestTimeout(DEFAULT_REQUEST_TIMEOUT));
239-
}
239+
}
240+
241+
//-----
242+
TEST_F(ClientUnitTests, AllowReceiptNodeFailoverDefaultIsFalse)
243+
{
244+
Client client;
245+
EXPECT_FALSE(client.getAllowReceiptNodeFailover());
246+
}
247+
248+
//-----
249+
TEST_F(ClientUnitTests, SetAllowReceiptNodeFailover)
250+
{
251+
Client client;
252+
client.setAllowReceiptNodeFailover(true);
253+
EXPECT_TRUE(client.getAllowReceiptNodeFailover());
254+
255+
client.setAllowReceiptNodeFailover(false);
256+
EXPECT_FALSE(client.getAllowReceiptNodeFailover());
257+
}

src/sdk/tests/unit/TransactionResponseUnitTests.cc

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
// SPDX-License-Identifier: Apache-2.0
2+
#include "AccountId.h"
3+
#include "Client.h"
4+
#include "TransactionReceiptQuery.h"
5+
#include "TransactionRecordQuery.h"
26
#include "TransactionResponse.h"
37

48
#include <gtest/gtest.h>
@@ -27,3 +31,50 @@ TEST_F(TransactionResponseUnitTests, ContructTransactionResponse)
2731
EXPECT_EQ(transactionResponse.mTransactionHash, hash);
2832
EXPECT_EQ(transactionResponse.mTransactionId, transactionId);
2933
}
34+
35+
//-----
36+
TEST_F(TransactionResponseUnitTests, GetReceiptQueryNullClientPinsToSubmittingNode)
37+
{
38+
// Given
39+
const AccountId submittingNode(0ULL, 0ULL, 3ULL);
40+
const TransactionId txId = TransactionId::withValidStart(AccountId(1ULL), std::chrono::system_clock::now());
41+
const TransactionResponse response(submittingNode, txId, {}, { submittingNode, AccountId(0ULL, 0ULL, 4ULL) });
42+
43+
// When / Then: no client → pinned to submitting node only
44+
EXPECT_EQ(response.getReceiptQuery(nullptr).getNodeAccountIds(), (std::vector<AccountId>{ submittingNode }));
45+
}
46+
47+
//-----
48+
TEST_F(TransactionResponseUnitTests, GetReceiptQueryFailoverBuildsNodeListFromTransactionNodes)
49+
{
50+
// Given: transaction nodes 3, 5, 4 (unsorted); submitting node is 3
51+
const AccountId submittingNode(0ULL, 0ULL, 3ULL);
52+
const AccountId node4(0ULL, 0ULL, 4ULL);
53+
const AccountId node5(0ULL, 0ULL, 5ULL);
54+
const TransactionId txId = TransactionId::withValidStart(AccountId(1ULL), std::chrono::system_clock::now());
55+
const TransactionResponse response(submittingNode, txId, {}, { submittingNode, node5, node4 });
56+
57+
Client client;
58+
client.setAllowReceiptNodeFailover(true);
59+
60+
// When / Then: submitting node first then sorting remaining by acc. number
61+
const std::vector<AccountId> expected = { submittingNode, node4, node5 };
62+
EXPECT_EQ(response.getReceiptQuery(&client).getNodeAccountIds(), expected);
63+
}
64+
65+
//-----
66+
TEST_F(TransactionResponseUnitTests, SingleFlagGovernsReceiptAndRecordQueryNodeList)
67+
{
68+
// Given
69+
const AccountId submittingNode(0ULL, 0ULL, 3ULL);
70+
const AccountId node4(0ULL, 0ULL, 4ULL);
71+
const TransactionId txId = TransactionId::withValidStart(AccountId(1ULL), std::chrono::system_clock::now());
72+
const TransactionResponse response(submittingNode, txId, {}, { submittingNode, node4 });
73+
74+
Client client;
75+
client.setAllowReceiptNodeFailover(true);
76+
77+
// When / Then: same flag, same node list for both query types
78+
EXPECT_EQ(response.getReceiptQuery(&client).getNodeAccountIds(),
79+
response.getRecordQuery(&client).getNodeAccountIds());
80+
}

0 commit comments

Comments
 (0)