diff --git a/src/sdk/main/include/ECDSAsecp256k1PrivateKey.h b/src/sdk/main/include/ECDSAsecp256k1PrivateKey.h index e6a3bc66..e7ce36c0 100644 --- a/src/sdk/main/include/ECDSAsecp256k1PrivateKey.h +++ b/src/sdk/main/include/ECDSAsecp256k1PrivateKey.h @@ -176,6 +176,18 @@ class ECDSAsecp256k1PrivateKey : public PrivateKey */ [[nodiscard]] std::vector 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& msgHash, + const std::vector& r, + const std::vector& s) const; + private: /** * Construct from a wrapped OpenSSL key object and optionally a chain code. diff --git a/src/sdk/main/include/EthereumFlow.h b/src/sdk/main/include/EthereumFlow.h index 5030a57a..d7c35c19 100644 --- a/src/sdk/main/include/EthereumFlow.h +++ b/src/sdk/main/include/EthereumFlow.h @@ -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: /** diff --git a/src/sdk/main/src/ECDSAsecp256k1PrivateKey.cc b/src/sdk/main/src/ECDSAsecp256k1PrivateKey.cc index f2b16054..6c596f56 100644 --- a/src/sdk/main/src/ECDSAsecp256k1PrivateKey.cc +++ b/src/sdk/main/src/ECDSAsecp256k1PrivateKey.cc @@ -312,4 +312,129 @@ ECDSAsecp256k1PrivateKey::ECDSAsecp256k1PrivateKey(internal::OpenSSLUtils::EVP_P { } +int ECDSAsecp256k1PrivateKey::calculateRecoveryId(const std::vector& msgHash, + const std::vector& rBytes, + const std::vector& sBytes) const +{ + if (rBytes.size() != 32 || sBytes.size() != 32) + { + return -1; + } + + // Convert r and s to BIGNUM + BIGNUM* r = BN_bin2bn(reinterpret_cast(rBytes.data()), 32, nullptr); + BIGNUM* s = BN_bin2bn(reinterpret_cast(sBytes.data()), 32, nullptr); + 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(getPublicKey()); + if (!pubKeyObj) + { + BN_free(r); + BN_free(s); + EC_GROUP_free(group); + BN_CTX_free(ctx); + BN_free(order); + return -1; + } + std::vector 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(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 recPubKey(65); + size_t len = EC_POINT_point2oct(group, Q, POINT_CONVERSION_UNCOMPRESSED, recPubKey.data(), recPubKey.size(), ctx); + if (len == 65) + { + std::vector 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 diff --git a/src/sdk/tests/integration/BaseIntegrationTest.h b/src/sdk/tests/integration/BaseIntegrationTest.h index b8de6384..3caacd5d 100644 --- a/src/sdk/tests/integration/BaseIntegrationTest.h +++ b/src/sdk/tests/integration/BaseIntegrationTest.h @@ -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. @@ -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 diff --git a/src/sdk/tests/integration/EthereumTransactionIntegrationTests.cc b/src/sdk/tests/integration/EthereumTransactionIntegrationTests.cc index d0405502..7a5bbca5 100644 --- a/src/sdk/tests/integration/EthereumTransactionIntegrationTests.cc +++ b/src/sdk/tests/integration/EthereumTransactionIntegrationTests.cc @@ -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" @@ -35,7 +29,7 @@ class EthereumTransactionIntegrationTests : public BaseIntegrationTest }; //----- -TEST_F(EthereumTransactionIntegrationTests, DISABLED_SignerNonceChangedOnEthereumTransaction) +TEST_F(EthereumTransactionIntegrationTests, SignerNonceChangedOnEthereumTransaction) { // Given const std::shared_ptr testPrivateKey = ECDSAsecp256k1PrivateKey::fromString( @@ -70,7 +64,7 @@ TEST_F(EthereumTransactionIntegrationTests, DISABLED_SignerNonceChangedOnEthereu ContractCreateTransaction() .setBytecodeFileId(fileId) .setAdminKey(getTestClient().getOperatorPublicKey()) - .setGas(200000ULL) + .setGas(2000000ULL) .setConstructorParameters(ContractFunctionParameters().addString("Hello from Hiero.").toBytes()) .setMemo(memo) .execute(getTestClient()) @@ -89,11 +83,12 @@ TEST_F(EthereumTransactionIntegrationTests, DISABLED_SignerNonceChangedOnEthereu std::vector callData = ContractFunctionParameters().addString("new message").toBytes("setMessage"); std::vector 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); @@ -113,25 +108,125 @@ TEST_F(EthereumTransactionIntegrationTests, DISABLED_SignerNonceChangedOnEthereu std::vector s(signedBytes.end() - std::min(signedBytes.size(), static_cast(32)), signedBytes.end()); - std::vector 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 recoveryId = { static_cast(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 ethereumTransactionData = list.write(); - // Type should be concatenated to RLP as this is a service side requirement + std::vector 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) +{ + // Given + const std::shared_ptr testPrivateKey = ECDSAsecp256k1PrivateKey::fromString( + "30540201010420ac318ea8ff8d991ab2f16172b4738e74dc35a56681199cfb1c0cb2e7cb560ffda00706052b8104000aa124032200036843f5" + "cb338bbb4cdb21b0da4ea739d910951d6e8a5f703d313efe31afe788f4"); + const std::shared_ptr testPublicKey = + std::dynamic_pointer_cast(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 type = internal::HexConverter::hexToBytes("02"); + std::vector chainId = internal::HexConverter::hexToBytes("012a"); + std::vector nonce = internal::HexConverter::hexToBytes("00"); + std::vector maxPriorityGas = internal::HexConverter::hexToBytes("00"); + std::vector maxGas = internal::HexConverter::hexToBytes("d1385c7bf0"); + std::vector gasLimit = internal::HexConverter::hexToBytes("3567e0"); + std::vector to = internal::HexConverter::hexToBytes(contractId.toSolidityAddress()); + std::vector value = internal::HexConverter::hexToBytes("00"); + std::vector jumboCallData(1024 * 120, std::byte(0)); + std::vector callData = + ContractFunctionParameters().addBytes(jumboCallData).toBytes("consumeLargeCalldata"); + + callData.insert(callData.end(), 32, std::byte(0)); + std::vector 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 signedBytes = + testPrivateKey->sign(internal::Utilities::concatenateVectors({ type, list.write() })); + std::vector r(signedBytes.begin(), + signedBytes.begin() + std::min(signedBytes.size(), static_cast(32))); + + std::vector s(signedBytes.end() - std::min(signedBytes.size(), static_cast(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 recoveryId = { static_cast(recId) }; + + RLPItem testList = list; + testList.pushBack(recoveryId); + testList.pushBack(r); + testList.pushBack(s); + + std::vector 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); } \ No newline at end of file