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
12 changes: 12 additions & 0 deletions src/sdk/main/include/ECDSAsecp256k1PrivateKey.h
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,18 @@ class ECDSAsecp256k1PrivateKey : public PrivateKey
*/
[[nodiscard]] std::vector<std::byte> toBytesRaw() const override;

/**
* Calculate the ECDSA recovery id for a given message hash and signature (r, s).
*
* @param msgHash The message hash that was signed.
* @param r The r value of the signature (32 bytes).
* @param s The s value of the signature (32 bytes).
* @return The recovery id (0-3) if found, or -1 if not found.
*/
int calculateRecoveryId(const std::vector<std::byte>& msgHash,
const std::vector<std::byte>& r,
const std::vector<std::byte>& s) const;

private:
/**
* Construct from a wrapped OpenSSL key object and optionally a chain code.
Expand Down
5 changes: 4 additions & 1 deletion src/sdk/main/include/EthereumFlow.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ class TransactionResponse;
namespace Hiero
{
/**
* @deprecated use EthereumTransaction instead. With the introduction of Jumbo transactions,
* it should always be less cost and more efficient to use EthereumTransaction instead.
*
* A helper class to execute an EthereumTransaction. This will use FileCreateTransaction and FileAppendTransaction as
* necessary to create a file with the call data followed by an EthereumTransaction to execute the EthereumData.
*/
class EthereumFlow
class [[deprecated("Use EthereumTransaction instead")]] EthereumFlow
{
public:
/**
Expand Down
125 changes: 125 additions & 0 deletions src/sdk/main/src/ECDSAsecp256k1PrivateKey.cc
Original file line number Diff line number Diff line change
Expand Up @@ -312,4 +312,129 @@
{
}

int ECDSAsecp256k1PrivateKey::calculateRecoveryId(const std::vector<std::byte>& msgHash,

Check failure on line 315 in src/sdk/main/src/ECDSAsecp256k1PrivateKey.cc

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/sdk/main/src/ECDSAsecp256k1PrivateKey.cc#L315

Method Hiero::ECDSAsecp256k1PrivateKey::calculateRecoveryId has 102 lines of code (limit is 100)

Check failure on line 315 in src/sdk/main/src/ECDSAsecp256k1PrivateKey.cc

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/sdk/main/src/ECDSAsecp256k1PrivateKey.cc#L315

Method Hiero::ECDSAsecp256k1PrivateKey::calculateRecoveryId has a cyclomatic complexity of 14 (limit is 12)
const std::vector<std::byte>& rBytes,
const std::vector<std::byte>& sBytes) const
{
if (rBytes.size() != 32 || sBytes.size() != 32)
{
return -1;
}

// Convert r and s to BIGNUM
BIGNUM* r = BN_bin2bn(reinterpret_cast<const unsigned char*>(rBytes.data()), 32, nullptr);
BIGNUM* s = BN_bin2bn(reinterpret_cast<const unsigned char*>(sBytes.data()), 32, nullptr);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just FYI there are some objects in impl/openssl_utils that take care of the garbage collection for these OpenSSL types. You can either use the ones there or add more to simplify the processing here

if (!r || !s)
{
if (r)
BN_free(r);
if (s)
BN_free(s);
return -1;
}

int recid = -1;
EC_GROUP* group = EC_GROUP_new_by_curve_name(NID_secp256k1);
BN_CTX* ctx = BN_CTX_new();
BIGNUM* order = BN_new();
EC_GROUP_get_order(group, order, ctx);

// Get this key's public key for comparison
auto pubKeyObj = std::dynamic_pointer_cast<const ECDSAsecp256k1PublicKey>(getPublicKey());
if (!pubKeyObj)
{
BN_free(r);
BN_free(s);
EC_GROUP_free(group);
BN_CTX_free(ctx);
BN_free(order);
return -1;
}
std::vector<std::byte> pubKeyBytes = pubKeyObj->toBytesRaw();

for (int i = 0; i < 4; ++i)
{
// x = r + (i / 2) * order
BIGNUM* x = BN_dup(r);
if (i >= 2)
{
BN_add(x, x, order);
}

// Try to construct R
EC_POINT* R = EC_POINT_new(group);
if (!EC_POINT_set_compressed_coordinates_GFp(group, R, x, i % 2, ctx))
{
EC_POINT_free(R);
BN_free(x);
continue;
}

// Check nR == infinity
EC_POINT* nR = EC_POINT_new(group);
EC_POINT_mul(group, nR, nullptr, R, order, ctx);
if (!EC_POINT_is_at_infinity(group, nR))
{
EC_POINT_free(R);
EC_POINT_free(nR);
BN_free(x);
continue;
}
EC_POINT_free(nR);

// r^-1 mod n
BIGNUM* r_inv = BN_mod_inverse(nullptr, r, order, ctx);
// e = msgHash as BIGNUM
BIGNUM* e = BN_bin2bn(reinterpret_cast<const unsigned char*>(msgHash.data()), msgHash.size(), nullptr);

// Q = r^-1 * (sR - eG)
EC_POINT* sR = EC_POINT_new(group);
EC_POINT_mul(group, sR, nullptr, R, s, ctx);

EC_POINT* eG = EC_POINT_new(group);
EC_POINT_mul(group, eG, e, nullptr, nullptr, ctx);
EC_POINT_invert(group, eG, ctx); // -eG

EC_POINT* Q = EC_POINT_new(group);
EC_POINT_add(group, Q, sR, eG, ctx);
EC_POINT_mul(group, Q, nullptr, Q, r_inv, ctx);

// Serialize recovered pubkey to compare
std::vector<unsigned char> recPubKey(65);
size_t len = EC_POINT_point2oct(group, Q, POINT_CONVERSION_UNCOMPRESSED, recPubKey.data(), recPubKey.size(), ctx);
if (len == 65)
{
std::vector<std::byte> recPubKeyBytes(recPubKey.size());
std::memcpy(recPubKeyBytes.data(), recPubKey.data(), recPubKey.size());
if (recPubKeyBytes == pubKeyBytes)
{
recid = i;
EC_POINT_free(R);
EC_POINT_free(sR);
EC_POINT_free(eG);
EC_POINT_free(Q);
BN_free(x);
BN_free(r_inv);
BN_free(e);
break;
}
}
EC_POINT_free(R);
EC_POINT_free(sR);
EC_POINT_free(eG);
EC_POINT_free(Q);
BN_free(x);
BN_free(r_inv);
BN_free(e);
}

BN_free(r);
BN_free(s);
EC_GROUP_free(group);
BN_CTX_free(ctx);
BN_free(order);

return recid;
}

} // namespace Hiero
18 changes: 18 additions & 0 deletions src/sdk/tests/integration/BaseIntegrationTest.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ class BaseIntegrationTest : public testing::Test
*/
[[nodiscard]] inline const std::string& getTestBigContents() const { return mBigContents; }

/** Get the test jumbo smart contract bytecode used in integration tests.
*
* @return The test jumbo smart contract bytecode in hex format.
*/
[[nodiscard]] inline const std::string& getTestJumboSmartContractBytecode() const
{
return mJumboSmartContractBytecode;
}

/** Set the test Client operator with the given account ID and private key.
*
* @param accountId The account ID of the operator.
Expand Down Expand Up @@ -245,6 +254,15 @@ class BaseIntegrationTest : public testing::Test
"egestas augue elit, sollicitudin accumsan massa lobortis ac. Curabitur placerat, dolor a aliquam maximus, velit "
"ipsum laoreet ligula, id ullamcorper lacus nibh eget nisl. Donec eget lacus venenatis enim consequat auctor vel "
"in.\n";
const std::string mJumboSmartContractBytecode =
"6080604052348015600e575f5ffd5b506101828061001c5f395ff3fe608060405234801561000f575f5ffd5b5060043610610029575f3560e0"
"1c80631e0a3f051461002d575b5f5ffd5b610047600480360381019061004291906100d0565b61005d565b6040516100549190610133565b60"
"405180910390f35b5f5f905092915050565b5f5ffd5b5f5ffd5b5f5ffd5b5f5ffd5b5f5ffd5b5f5f83601f8401126100905761008f61006f56"
"5b5b8235905067ffffffffffffffff8111156100ad576100ac610073565b5b6020830191508360018202830111156100c9576100c861007756"
"5b5b9250929050565b5f5f602083850312156100e6576100e5610067565b5b5f83013567ffffffffffffffff8111156101035761010261006b"
"565b5b61010f8582860161007b565b92509250509250929050565b5f819050919050565b61012d8161011b565b82525050565b5f6020820190"
"506101465f830184610124565b9291505056fea26469706673582212202829ebd1cf38c443e4fd3770cd4306ac4c6bb9ac2828074ae2b9cd16"
"121fcfea64736f6c634300081e0033";
};

} // namespace Hiero
Expand Down
133 changes: 114 additions & 19 deletions src/sdk/tests/integration/EthereumTransactionIntegrationTests.cc
Original file line number Diff line number Diff line change
@@ -1,26 +1,20 @@
// SPDX-License-Identifier: Apache-2.0
#include "AccountCreateTransaction.h"
#include "AccountDeleteTransaction.h"
#include "AccountInfo.h"
#include "AccountInfoQuery.h"
#include "BaseIntegrationTest.h"
#include "Client.h"
#include "ContractCreateTransaction.h"
#include "ContractDeleteTransaction.h"
#include "ContractFunctionParameters.h"
#include "ContractId.h"
#include "ECDSAsecp256k1PrivateKey.h"
#include "ECDSAsecp256k1PublicKey.h"
#include "ED25519PrivateKey.h"
#include "EthereumTransaction.h"
#include "FileCreateTransaction.h"
#include "FileDeleteTransaction.h"
#include "FileId.h"
#include "TransactionReceipt.h"
#include "TransactionRecord.h"
#include "TransactionResponse.h"
#include "TransferTransaction.h"
#include "exceptions/OpenSSLException.h"
#include "impl/HexConverter.h"
#include "impl/RLPItem.h"
#include "impl/Utilities.h"
Expand All @@ -35,7 +29,7 @@
};

//-----
TEST_F(EthereumTransactionIntegrationTests, DISABLED_SignerNonceChangedOnEthereumTransaction)
TEST_F(EthereumTransactionIntegrationTests, SignerNonceChangedOnEthereumTransaction)

Check warning on line 32 in src/sdk/tests/integration/EthereumTransactionIntegrationTests.cc

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/sdk/tests/integration/EthereumTransactionIntegrationTests.cc#L32

Method TEST_F has 81 lines of code (limit is 50)
{
// Given
const std::shared_ptr<ECDSAsecp256k1PrivateKey> testPrivateKey = ECDSAsecp256k1PrivateKey::fromString(
Expand Down Expand Up @@ -70,7 +64,7 @@
ContractCreateTransaction()
.setBytecodeFileId(fileId)
.setAdminKey(getTestClient().getOperatorPublicKey())
.setGas(200000ULL)
.setGas(2000000ULL)
.setConstructorParameters(ContractFunctionParameters().addString("Hello from Hiero.").toBytes())
.setMemo(memo)
.execute(getTestClient())
Expand All @@ -89,11 +83,12 @@
std::vector<std::byte> callData = ContractFunctionParameters().addString("new message").toBytes("setMessage");
std::vector<std::byte> accessList = {};

// When
// Serialize bytes to RLP format for signing
RLPItem list(RLPItem::RLPType::LIST_TYPE);
list.pushBack(chainId);
list.pushBack(RLPItem());
list.pushBack(maxPriorityGas);
list.pushBack(RLPItem());
list.pushBack(maxGas);
list.pushBack(gasLimit);
list.pushBack(to);
Expand All @@ -113,25 +108,125 @@
std::vector<std::byte> s(signedBytes.end() - std::min(signedBytes.size(), static_cast<size_t>(32)),
signedBytes.end());

std::vector<std::byte> recoveryId = internal::HexConverter::hexToBytes("01");
// Calculate recovery id
int recId =
testPrivateKey->calculateRecoveryId(internal::Utilities::concatenateVectors({ type, list.write() }), r, s);
ASSERT_NE(recId, -1) << "Failed to calculate recovery id";
std::vector<std::byte> recoveryId = { static_cast<std::byte>(recId) };

// recId, r, s should be added to original RLP list as Ethereum Transactions require
list.pushBack(recoveryId);
list.pushBack(r);
list.pushBack(s);
RLPItem testList = list;
testList.pushBack(recoveryId);
testList.pushBack(r);
testList.pushBack(s);

std::vector<std::byte> ethereumTransactionData = list.write();
// Type should be concatenated to RLP as this is a service side requirement
std::vector<std::byte> ethereumTransactionData = testList.write();
ethereumTransactionData = internal::Utilities::concatenateVectors({ type, ethereumTransactionData });

// When Then
EthereumTransaction ethereumTransaction;
EXPECT_NO_THROW(ethereumTransaction = EthereumTransaction().setEthereumData(ethereumTransactionData));

TransactionResponse txResponse;
EXPECT_NO_THROW(txResponse = ethereumTransaction.execute(getTestClient()));
EXPECT_TRUE(txResponse.getRecord(getTestClient()).mContractFunctionResult.has_value());
EXPECT_EQ(txResponse.getRecord(getTestClient()).mContractFunctionResult.value().mSignerNonce, 1);
}

//-----
TEST_F(EthereumTransactionIntegrationTests, JumboTransaction)

Check warning on line 134 in src/sdk/tests/integration/EthereumTransactionIntegrationTests.cc

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/sdk/tests/integration/EthereumTransactionIntegrationTests.cc#L134

Method TEST_F has 80 lines of code (limit is 50)
{
// Given
const std::shared_ptr<ECDSAsecp256k1PrivateKey> testPrivateKey = ECDSAsecp256k1PrivateKey::fromString(
"30540201010420ac318ea8ff8d991ab2f16172b4738e74dc35a56681199cfb1c0cb2e7cb560ffda00706052b8104000aa124032200036843f5"
"cb338bbb4cdb21b0da4ea739d910951d6e8a5f703d313efe31afe788f4");
const std::shared_ptr<ECDSAsecp256k1PublicKey> testPublicKey =
std::dynamic_pointer_cast<ECDSAsecp256k1PublicKey>(testPrivateKey->getPublicKey());
const AccountId aliasAccountId = testPublicKey->toAccountId();

TransactionReceipt aliasTransferTxReciept;
EXPECT_NO_THROW(aliasTransferTxReciept =
TransferTransaction()
.addHbarTransfer(getTestClient().getOperatorAccountId().value(), Hbar(1LL).negated())
.addHbarTransfer(aliasAccountId, Hbar(1LL))
.execute(getTestClient())
.getReceipt(getTestClient()));

AccountInfo accountInfo;
EXPECT_NO_THROW(accountInfo = AccountInfoQuery().setAccountId(aliasAccountId).execute(getTestClient()));

FileId fileId;
EXPECT_NO_THROW(fileId = FileCreateTransaction()
.setKeys({ getTestClient().getOperatorPublicKey() })
.setContents(internal::Utilities::stringToByteVector(getTestJumboSmartContractBytecode()))
.execute(getTestClient())
.getReceipt(getTestClient())
.mFileId.value());

ContractId contractId;
EXPECT_NO_THROW(contractId = ContractCreateTransaction()
.setBytecodeFileId(fileId)
.setAdminKey(getTestClient().getOperatorPublicKey())
.setGas(2000000ULL)
.execute(getTestClient())
.getReceipt(getTestClient())
.mContractId.value());

std::vector<std::byte> type = internal::HexConverter::hexToBytes("02");
std::vector<std::byte> chainId = internal::HexConverter::hexToBytes("012a");
std::vector<std::byte> nonce = internal::HexConverter::hexToBytes("00");
std::vector<std::byte> maxPriorityGas = internal::HexConverter::hexToBytes("00");
std::vector<std::byte> maxGas = internal::HexConverter::hexToBytes("d1385c7bf0");
std::vector<std::byte> gasLimit = internal::HexConverter::hexToBytes("3567e0");
std::vector<std::byte> to = internal::HexConverter::hexToBytes(contractId.toSolidityAddress());
std::vector<std::byte> value = internal::HexConverter::hexToBytes("00");
std::vector<std::byte> jumboCallData(1024 * 120, std::byte(0));
std::vector<std::byte> callData =
ContractFunctionParameters().addBytes(jumboCallData).toBytes("consumeLargeCalldata");

callData.insert(callData.end(), 32, std::byte(0));
std::vector<std::byte> accessList = {};

// When
// Serialize bytes to RLP format for signing
RLPItem list(RLPItem::RLPType::LIST_TYPE);
list.pushBack(chainId);
list.pushBack(RLPItem());
list.pushBack(RLPItem());
list.pushBack(maxGas);
list.pushBack(gasLimit);
list.pushBack(to);
list.pushBack(RLPItem());
list.pushBack(callData);
RLPItem accessListItem(accessList);
accessListItem.setType(RLPItem::RLPType::LIST_TYPE);
list.pushBack(accessListItem);

// signed bytes in r,s form
std::vector<std::byte> signedBytes =
testPrivateKey->sign(internal::Utilities::concatenateVectors({ type, list.write() }));

std::vector<std::byte> r(signedBytes.begin(),
signedBytes.begin() + std::min(signedBytes.size(), static_cast<size_t>(32)));

std::vector<std::byte> s(signedBytes.end() - std::min(signedBytes.size(), static_cast<size_t>(32)),
signedBytes.end());

// Calculate recovery id
int recId =
testPrivateKey->calculateRecoveryId(internal::Utilities::concatenateVectors({ type, list.write() }), r, s);
ASSERT_NE(recId, -1) << "Failed to calculate recovery id";
std::vector<std::byte> recoveryId = { static_cast<std::byte>(recId) };

RLPItem testList = list;
testList.pushBack(recoveryId);
testList.pushBack(r);
testList.pushBack(s);

std::vector<std::byte> ethereumTransactionData = testList.write();
ethereumTransactionData = internal::Utilities::concatenateVectors({ type, ethereumTransactionData });

EthereumTransaction ethereumTransaction;
EXPECT_NO_THROW(ethereumTransaction = EthereumTransaction().setEthereumData(ethereumTransactionData));
TransactionResponse txResponse;
EXPECT_NO_THROW(txResponse = ethereumTransaction.execute(getTestClient()));
EXPECT_TRUE(txResponse.getRecord(getTestClient()).mContractFunctionResult.has_value());
// mSignerNonce should be incremented to 1 after the first contract execution
EXPECT_EQ(txResponse.getRecord(getTestClient()).mContractFunctionResult.value().mSignerNonce, 1);
}
Loading