Skip to content
Draft
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
33 changes: 33 additions & 0 deletions src/sdk/main/include/Transaction.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ class TransactionResponse;

namespace Hiero
{

struct SignableNodeTransactionBodyBytes {
AccountId nodeId;
std::vector<std::byte> body;
TransactionId transactionId;
};

/**
* Base class for all transactions that can be submitted to a Hiero network.
*
Expand Down Expand Up @@ -154,6 +161,22 @@ class Transaction
virtual SdkRequestType& addSignature(const std::shared_ptr<PublicKey>& publicKey,
const std::vector<std::byte>& signature);

/**
* Add a signature to this Transaction for a specific transaction ID and node account ID. This is useful for signing
* chunked transactions that have multiple transaction IDs.
*
* @param publicKey The public key that is adding a signature.
* @param signature The signature to add.
* @param transactionId The ID of the transaction to which to add the signature.
* @param nodeId The ID of the node account for the transaction to which to add the signature.
* @return A reference to this derived Transaction object with the newly-added signature.
* @throws IllegalStateException If this Transaction is not frozen.
*/
virtual SdkRequestType& addSignature(const std::shared_ptr<PublicKey>& publicKey,
const std::vector<std::byte>& signature,
const TransactionId& transactionId,
const AccountId& nodeId);

/**
* Get the signatures of each potential Transaction protobuf object this Transaction may send.
*
Expand Down Expand Up @@ -330,6 +353,16 @@ class Transaction
*/
[[nodiscard]] size_t getTransactionBodySize() const;

/**
* Get a list of signable node transaction body bytes for each signed transaction in the transaction list.
* The NodeId represents the node that this transaction is signed for.
* The TransactionId is useful for signing chunked transactions like FileAppendTransaction, since they can have multiple transaction ids.
* @return A vector of SignableNodeTransactionBodyBytes, one for each signed transaction.
* @throws IllegalStateException If this Transaction is not frozen.
* @throws std::runtime_error If parsing the TransactionBody fails.
*/
[[nodiscard]] std::vector<SignableNodeTransactionBodyBytes> getSignableNodeBodyBytesList() const;

protected:
/**
* Dummy transaction and account IDs used to assist in deserializing incomplete Transactions.
Expand Down
94 changes: 93 additions & 1 deletion src/sdk/main/src/Transaction.cc
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,7 @@
Executable<SdkRequestType, proto::Transaction, proto::TransactionResponse, TransactionResponse>::setNodeAccountIds(
{ DUMMY_ACCOUNT_ID });

// Sign the transaction with the clients operator, if applicable
// Sign the transaction with the client's operator, if applicable
if (client.getOperatorAccountId().has_value())
{
signWithOperator(client);
Expand Down Expand Up @@ -495,6 +495,71 @@
return static_cast<SdkRequestType&>(*this);
}

//-----
template<typename SdkRequestType>
SdkRequestType& Transaction<SdkRequestType>::addSignature(const std::shared_ptr<PublicKey>& publicKey,

Check failure on line 500 in src/sdk/main/src/Transaction.cc

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/sdk/main/src/Transaction.cc#L500

Method Hiero::Transaction<SdkRequestType>::addSignature has a cyclomatic complexity of 13 (limit is 12)
const std::vector<std::byte>& signature,
const TransactionId& transactionId,
const AccountId& nodeId)
{
// A signature can only be added to frozen Transactions.
if (!isFrozen())
{
throw IllegalStateException("Adding a signature to a Transaction requires the Transaction to be frozen");
}

bool signedAtLeastOne = false;
for (auto& signedTx : mImpl->mSignedTransactions)
{
proto::TransactionBody txBody;
if (!txBody.ParseFromString(signedTx.bodybytes()))
{
// This should not happen with a correctly constructed transaction.
throw std::runtime_error("Failed to parse TransactionBody from SignedTransaction body bytes.");
}

if (txBody.has_transactionid() && txBody.has_nodeaccountid() &&
TransactionId::fromProtobuf(txBody.transactionid()) == transactionId &&
AccountId::fromProtobuf(txBody.nodeaccountid()) == nodeId)
{
// Check if this public key has already signed this specific SignedTransaction
bool alreadySigned = false;
const std::string pkBytesStr = internal::Utilities::byteVectorToString(publicKey->toBytesRaw());
for (const auto& sigPair : signedTx.sigmap().sigpair())
{
if (sigPair.pubkeyprefix() == pkBytesStr)
{
alreadySigned = true;
break;
}
}

if (!alreadySigned)
{
*signedTx.mutable_sigmap()->add_sigpair() = *publicKey->toSignaturePairProtobuf(signature);
signedAtLeastOne = true;
}
}
}

if (signedAtLeastOne)
{
// Adding a signature will require all Transaction protobuf objects to be regenerated.
mImpl->mTransactions.clear();
mImpl->mTransactions.resize(mImpl->mSignedTransactions.size());

// If this key hasn't been seen before for this whole transaction, add it to the list of signatories so it's known
// this key is a signer.
if (!keyAlreadySigned(publicKey))
{
mImpl->mSignatories.emplace(publicKey, std::function<std::vector<std::byte>(const std::vector<std::byte>&)>());
mImpl->mPrivateKeys.emplace(publicKey, nullptr);
}
}

return static_cast<SdkRequestType&>(*this);
}

//-----
template<typename SdkRequestType>
std::map<AccountId, std::map<std::shared_ptr<PublicKey>, std::vector<std::byte>>>
Expand Down Expand Up @@ -1379,6 +1444,33 @@
return static_cast<SdkRequestType&>(*this);
}

//-----
template<typename SdkRequestType>
std::vector<SignableNodeTransactionBodyBytes> Transaction<SdkRequestType>::getSignableNodeBodyBytesList() const
{
if (!isFrozen()) {
throw IllegalStateException("Transaction must be frozen in order to get signable node body bytes.");
}

std::vector<SignableNodeTransactionBodyBytes> result;
result.reserve(mImpl->mSignedTransactions.size());

for (const auto& signedTx : mImpl->mSignedTransactions) {
proto::TransactionBody body;
if (!body.ParseFromString(signedTx.bodybytes())) {
throw std::runtime_error("Failed to parse TransactionBody from SignedTransaction body bytes.");
}
AccountId nodeId = body.has_nodeaccountid() ? AccountId::fromProtobuf(body.nodeaccountid()) : AccountId();
TransactionId transactionId = body.has_transactionid() ? TransactionId::fromProtobuf(body.transactionid()) : TransactionId();
result.push_back(SignableNodeTransactionBodyBytes{
nodeId,
internal::Utilities::stringToByteVector(signedTx.bodybytes()),
transactionId
});
}
return result;
}

/**
* Explicit template instantiations.
*/
Expand Down
136 changes: 136 additions & 0 deletions src/sdk/tests/unit/TransactionUnitTests.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2819,4 +2819,140 @@

// Then
ASSERT_GT(transactionBigBodySize, transactionSmallBodySize);
}

//-----
TEST_F(TransactionUnitTests, GetSignableNodeBodyBytesListWorksForFrozenTransaction)
{
// Given
AccountId nodeId(1ULL, 2ULL, 3ULL);
TransactionId txId = TransactionId::generate(AccountId(7ULL, 8ULL, 9ULL));
AccountCreateTransaction transaction;
transaction.setNodeAccountIds({ nodeId });
transaction.setTransactionId(txId);
transaction.freeze();

// When
std::vector<SignableNodeTransactionBodyBytes> result;
ASSERT_NO_THROW(result = transaction.getSignableNodeBodyBytesList());

// Then
ASSERT_EQ(result.size(), 1U);
EXPECT_EQ(result[0].nodeId, nodeId);
EXPECT_EQ(result[0].transactionId, txId);
EXPECT_FALSE(result[0].body.empty());
}

//-----
TEST_F(TransactionUnitTests, GetSignableNodeBodyBytesListThrowsIfNotFrozen)
{
// Given
AccountCreateTransaction transaction;
// Not frozen

// When / Then
EXPECT_THROW(transaction.getSignableNodeBodyBytesList(), IllegalStateException);
}

//-----
TEST_F(TransactionUnitTests, AddSignatureV2AddsSignatureToCorrectChunk)

Check warning on line 2858 in src/sdk/tests/unit/TransactionUnitTests.cc

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/sdk/tests/unit/TransactionUnitTests.cc#L2858

Method TEST_F has a cyclomatic complexity of 11 (limit is 8)
{
// Given
AccountId nodeId1(1ULL, 2ULL, 3ULL);
AccountId nodeId2(4ULL, 5ULL, 6ULL);
TransactionId txId1 = TransactionId::generate(AccountId(7ULL, 8ULL, 9ULL));
TransactionId txId2 = TransactionId::generate(AccountId(10ULL, 11ULL, 12ULL));
std::shared_ptr<ED25519PrivateKey> privKey = ED25519PrivateKey::generatePrivateKey();
std::shared_ptr<PublicKey> pubKey = privKey->getPublicKey();
std::vector<std::byte> signature = privKey->sign({std::byte{0x01}, std::byte{0x02}});

// Build proto objects for each chunk
proto::TransactionBody txBody1, txBody2;
txBody1.set_allocated_cryptocreateaccount(new proto::CryptoCreateTransactionBody());
txBody2.set_allocated_cryptocreateaccount(new proto::CryptoCreateTransactionBody());
txBody1.set_allocated_transactionid(txId1.toProtobuf().release());
txBody2.set_allocated_transactionid(txId2.toProtobuf().release());

proto::SignedTransaction signedTx1, signedTx2;
signedTx1.set_bodybytes(txBody1.SerializeAsString());
signedTx2.set_bodybytes(txBody2.SerializeAsString());

proto::Transaction protoTx1, protoTx2;
protoTx1.set_signedtransactionbytes(signedTx1.SerializeAsString());
protoTx2.set_signedtransactionbytes(signedTx2.SerializeAsString());

std::map<TransactionId, std::map<AccountId, proto::Transaction>> transactions;
transactions[txId1][nodeId1] = protoTx1;
transactions[txId2][nodeId2] = protoTx2;

AccountCreateTransaction tx(transactions);
tx.freeze();

// When: Add signature to chunk with txId2 and nodeId2
EXPECT_NO_THROW(tx.addSignature(pubKey, signature, txId2, nodeId2));

// Then: Only the second chunk should have the signature
// Use getSignatures() to check
auto signatures = tx.getSignatures();
// nodeId1 chunk should not have the signature
bool found1 = false;
if (signatures.count(nodeId1)) {
for (const auto& [key, sig] : signatures[nodeId1]) {
if (key && pubKey && key->toBytesRaw() == pubKey->toBytesRaw()) found1 = true;
}
}
// nodeId2 chunk should have the signature
bool found2 = false;
if (signatures.count(nodeId2)) {
for (const auto& [key, sig] : signatures[nodeId2]) {
if (key && pubKey && key->toBytesRaw() == pubKey->toBytesRaw()) found2 = true;
}
}
EXPECT_FALSE(found1);
EXPECT_TRUE(found2);
}

//-----
TEST_F(TransactionUnitTests, AddSignatureV2ThrowsIfNotFrozen)
{
// Given
AccountCreateTransaction tx;
std::shared_ptr<ED25519PrivateKey> privKey = ED25519PrivateKey::generatePrivateKey();
std::shared_ptr<PublicKey> pubKey = privKey->getPublicKey();
std::vector<std::byte> signature = privKey->sign({std::byte{0x01}, std::byte{0x02}});
TransactionId txId = TransactionId::generate(AccountId(1ULL));
AccountId nodeId(2ULL);

// When / Then
EXPECT_THROW(tx.addSignature(pubKey, signature, txId, nodeId), IllegalStateException);
}

//-----
TEST_F(TransactionUnitTests, AddSignatureV2DoesNotDuplicateSignature)
{
// Given
AccountId nodeId(1ULL, 2ULL, 3ULL);
TransactionId txId = TransactionId::generate(AccountId(7ULL, 8ULL, 9ULL));
std::shared_ptr<ED25519PrivateKey> privKey = ED25519PrivateKey::generatePrivateKey();
std::shared_ptr<PublicKey> pubKey = privKey->getPublicKey();
std::vector<std::byte> signature = privKey->sign({std::byte{0x01}, std::byte{0x02}});
AccountCreateTransaction tx;
tx.setNodeAccountIds({nodeId});
tx.setTransactionId(txId);
tx.freeze();

// Add signature once
EXPECT_NO_THROW(tx.addSignature(pubKey, signature, txId, nodeId));
// Add signature again
EXPECT_NO_THROW(tx.addSignature(pubKey, signature, txId, nodeId));

// Then: Only one signature should be present
auto signatures = tx.getSignatures();
int count = 0;
if (signatures.count(nodeId)) {
for (const auto& [key, sig] : signatures[nodeId]) {
if (key && pubKey && key->toBytesRaw() == pubKey->toBytesRaw()) count++;
}
}
EXPECT_EQ(count, 1);
}
Loading