From c0d03780deaee6d512d96aa924d60a50ff361e28 Mon Sep 17 00:00:00 2001 From: doylet Date: Tue, 16 Apr 2024 15:52:13 +1000 Subject: [PATCH 1/6] Fix padToNBytes not working with 0 size strings, reduce allocs Using operator= for the LHS means the string is reassigned instead of potentially using available capacity in the pre-existing string. You can use the `insert` or `append` calls to reuse the internal buffer where possible. --- include/ethyl/utils.hpp | 2 +- src/utils.cpp | 17 +++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/include/ethyl/utils.hpp b/include/ethyl/utils.hpp index 4d0986c..918fd25 100644 --- a/include/ethyl/utils.hpp +++ b/include/ethyl/utils.hpp @@ -45,7 +45,7 @@ namespace utils std::string getFunctionSignature(const std::string& function); - std::string padToNBytes(const std::string& input, size_t byte_count, PaddingDirection direction = PaddingDirection::LEFT); + std::string padToNBytes(const std::string& input, size_t byteCount, PaddingDirection direction = PaddingDirection::LEFT); std::string padTo8Bytes(const std::string& input, PaddingDirection direction = PaddingDirection::LEFT); std::string padTo32Bytes(const std::string& input, PaddingDirection direction = PaddingDirection::LEFT); diff --git a/src/utils.cpp b/src/utils.cpp index 79d2fbd..dd76cea 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -81,7 +81,7 @@ std::string utils::getFunctionSignature(const std::string& function) { return "0x" + hashHex.substr(0, 8); } -std::string utils::padToNBytes(const std::string& input, size_t byte_count, utils::PaddingDirection direction) { +std::string utils::padToNBytes(const std::string& input, size_t byteCount, utils::PaddingDirection direction) { std::string output = input; bool has0xPrefix = false; @@ -92,20 +92,21 @@ std::string utils::padToNBytes(const std::string& input, size_t byte_count, util } // Calculate padding size based on byteCount * 2 (since each byte is represented by 2 hex characters) - size_t targetHexStringSize = byte_count * 2; - size_t nextMultiple = (output.size() + targetHexStringSize - 1) / targetHexStringSize * targetHexStringSize; - size_t paddingSize = nextMultiple - output.size(); - std::string padding(paddingSize, '0'); + const size_t targetHexStringSize = byteCount * 2; + const size_t startingSize = std::max(output.size(), static_cast(1)); // Size is atleast 1 element such that we handle when output.size == 0 + const size_t startingSizeRoundedUp = startingSize + (targetHexStringSize - 1); + const size_t nextMultiple = /*floor*/ (startingSizeRoundedUp / targetHexStringSize) * targetHexStringSize; + const size_t paddingSize = nextMultiple - output.size(); if (direction == PaddingDirection::LEFT) { - output = padding + output; + output.insert(0, paddingSize, '0'); } else { - output += padding; + output.append(paddingSize, '0'); } // If input started with "0x", add it back if (has0xPrefix) { - output = "0x" + output; + output.insert(0, "0x"); } return output; From 50661198c511992117d3ae008ebc5ac1115ac32f Mon Sep 17 00:00:00 2001 From: doylet Date: Tue, 16 Apr 2024 15:58:18 +1000 Subject: [PATCH 2/6] Add `callReadFunctionJSON` to return the JSON object from a RPC call Allow returning of the original JSON object which allows the caller avoid a memory allocation and compute cost of automatically converting the JSON to a string. It also the caller to preserve the JSON structure for lookup. Additionally, the caller can get references to the underlying data avoiding additional allocations. --- include/ethyl/provider.hpp | 8 +++++--- src/provider.cpp | 34 +++++++++++++++++++++------------- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/include/ethyl/provider.hpp b/include/ethyl/provider.hpp index a772b35..c2297f4 100644 --- a/include/ethyl/provider.hpp +++ b/include/ethyl/provider.hpp @@ -2,6 +2,7 @@ #pragma once #include +#include #include #include @@ -38,9 +39,10 @@ class Provider { void connectToNetwork(); void disconnectFromNetwork(); - uint64_t getTransactionCount(const std::string& address, const std::string& blockTag); - std::string callReadFunction(const ReadCallData& callData, uint64_t blockNumberInt); - std::string callReadFunction(const ReadCallData& callData, const std::string& blockNumber = "latest"); + uint64_t getTransactionCount(const std::string& address, const std::string& blockTag); + nlohmann::json callReadFunctionJSON(const ReadCallData& callData, std::string_view blockNumber = "latest"); + std::string callReadFunction(const ReadCallData& callData, std::string_view blockNumber = "latest"); + std::string callReadFunction(const ReadCallData& callData, uint64_t blockNumberInt); uint32_t getNetworkChainId(); std::string evm_snapshot(); diff --git a/src/provider.cpp b/src/provider.cpp index 5caa890..aeac049 100644 --- a/src/provider.cpp +++ b/src/provider.cpp @@ -48,32 +48,40 @@ cpr::Response Provider::makeJsonRpcRequest(const std::string& method, const nloh return session.Post(); } -std::string Provider::callReadFunction(const ReadCallData& callData, uint64_t blockNumberInt) { - std::stringstream stream; - stream << "0x" << std::hex << blockNumberInt; // Convert uint64_t to hex string - std::string blockNumberHex = stream.str(); - - return callReadFunction(callData, blockNumberHex); // Call the original function -} +nlohmann::json Provider::callReadFunctionJSON(const ReadCallData& callData, std::string_view blockNumber) { + nlohmann::json result = {}; -std::string Provider::callReadFunction(const ReadCallData& callData, const std::string& blockNumber) { // Prepare the params for the eth_call request - nlohmann::json params = nlohmann::json::array(); - params[0]["to"] = callData.contractAddress; - params[0]["data"] = callData.data; - params[1] = blockNumber; // use the provided block number or default to "latest" + nlohmann::json params = nlohmann::json::array(); + params[0]["to"] = callData.contractAddress; + params[0]["data"] = callData.data; + params[1] = blockNumber; // use the provided block number or default to "latest" cpr::Response response = makeJsonRpcRequest("eth_call", params); if (response.status_code == 200) { nlohmann::json responseJson = nlohmann::json::parse(response.text); if (!responseJson["result"].is_null()) { - return responseJson["result"]; + result = responseJson["result"]; + return result; } } throw std::runtime_error("Unable to get the result of the function call"); } +std::string Provider::callReadFunction(const ReadCallData& callData, std::string_view blockNumber) { + std::string result = callReadFunctionJSON(callData, blockNumber); + return result; +} + +std::string Provider::callReadFunction(const ReadCallData& callData, uint64_t blockNumberInt) { + std::stringstream stream; + stream << "0x" << std::hex << blockNumberInt; // Convert uint64_t to hex string + std::string blockNumberHex = stream.str(); + std::string result = callReadFunctionJSON(callData, blockNumberHex); + return result; +} + uint32_t Provider::getNetworkChainId() { // Make the request takes no params nlohmann::json params = nlohmann::json::array(); From 56ceca62a9a3b011d4182a66130bcfe045073ab3 Mon Sep 17 00:00:00 2001 From: doylet Date: Tue, 16 Apr 2024 16:01:52 +1000 Subject: [PATCH 3/6] Add conversion from BLS key to address in binary form Additionally rename the functions to be the format to which allows parsing of intent from left-to-right. --- include/ethyl/signer.hpp | 3 ++- src/signer.cpp | 18 +++++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/include/ethyl/signer.hpp b/include/ethyl/signer.hpp index 3d1073b..2fca531 100644 --- a/include/ethyl/signer.hpp +++ b/include/ethyl/signer.hpp @@ -24,7 +24,8 @@ class Signer { // Returns std::pair, std::vector> generate_key_pair(); - std::string addressFromPrivateKey(const std::vector& seckey); + std::array secretKeyToAddress(const std::vector& seckey); + std::string secretKeyToAddressString(const std::vector& seckey); std::vector sign(const std::array& hash, const std::vector& seckey); std::vector sign(const std::string& hash, const std::vector& seckey); diff --git a/src/signer.cpp b/src/signer.cpp index 06ce23c..3bc01cd 100644 --- a/src/signer.cpp +++ b/src/signer.cpp @@ -60,7 +60,7 @@ std::pair, std::vector> Signer::genera std::vector(compressed_pubkey, compressed_pubkey + sizeof(compressed_pubkey))}; } -std::string Signer::addressFromPrivateKey(const std::vector& seckey) { +std::array Signer::secretKeyToAddress(const std::vector& seckey) { std::string address; // Verify the private key. @@ -84,10 +84,18 @@ std::string Signer::addressFromPrivateKey(const std::vector& seck auto hashed_pub = utils::hash(pub_string); // The last 20 bytes of the Keccak-256 hash of the public key in hex is the address. - address = utils::toHexString(hashed_pub); - address = address.substr(address.size() - 40); + std::array result = {}; + std::memcpy(result.data(), hashed_pub.data() + hashed_pub.size() - result.size(), result.size()); + return result; +} - return "0x" + address; +std::string Signer::secretKeyToAddressString(const std::vector& seckey) { + std::array address = secretKeyToAddress(seckey); + std::string result = {}; + result.reserve(2 + (address.max_size() * 2)); + result += "0x"; + result += utils::toHexString(address); + return result; } @@ -173,7 +181,7 @@ std::string Signer::signTransaction(Transaction& txn, const std::vector& seckey) { - const auto senders_address = addressFromPrivateKey(seckey); + const auto senders_address = secretKeyToAddressString(seckey); populateTransaction(txn, senders_address); const auto signature_hex = utils::toHexString(sign(txn.hash(), seckey)); From 8e871e57794d86c444d32adaec172a1ff913e88d Mon Sep 17 00:00:00 2001 From: doylet Date: Tue, 16 Apr 2024 16:08:21 +1000 Subject: [PATCH 4/6] Add more helper functions into utils, trimPrefix, trimLeaderZeros.. - Take in string_view where possible to avoid allocations where possible - Since we take string_views, the previous functions used from the standard library only worked with strings/or required cstrings which are null-terminated, hence we have to transform our functions to support length denoted strings as not all string_view's are null-terminated. --- include/ethyl/utils.hpp | 11 ++--- src/utils.cpp | 92 +++++++++++++++++++++++++++++++---------- 2 files changed, 77 insertions(+), 26 deletions(-) diff --git a/include/ethyl/utils.hpp b/include/ethyl/utils.hpp index 918fd25..c29ee4c 100644 --- a/include/ethyl/utils.hpp +++ b/include/ethyl/utils.hpp @@ -34,12 +34,13 @@ namespace utils return oss.str(); } - std::string decimalToHex(uint64_t decimal); + std::string decimalToHex(uint64_t decimal); + std::string_view trimPrefix(std::string_view src, std::string_view prefix); + std::string_view trimLeadingZeros(std::string_view src); - std::vector fromHexString(std::string hex_str); - uint64_t fromHexStringToUint64(std::string hex_str); - - std::array fromHexString32Byte(std::string hex_str); + std::vector fromHexString(std::string_view hexStr); + uint64_t fromHexStringToUint64(std::string_view hexStr); + std::array fromHexString32Byte(std::string_view hexStr); std::array hash(std::string in); diff --git a/src/utils.cpp b/src/utils.cpp index dd76cea..f6d6bdc 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -4,6 +4,7 @@ #include #include #include +#include extern "C" { #include "crypto/keccak.h" @@ -15,37 +16,86 @@ std::string utils::decimalToHex(uint64_t decimal) { return ss.str(); } -std::vector utils::fromHexString(std::string hex_str) { - std::vector bytes; - - // Check for "0x" prefix and remove it - if(hex_str.size() >= 2 && hex_str[0] == '0' && hex_str[1] == 'x') { - hex_str = hex_str.substr(2); +std::string_view utils::trimPrefix(std::string_view src, std::string_view prefix) +{ + std::string_view result = src; + if (result.size() >= prefix.size()) { + if (result.substr(0, prefix.size()) == prefix) { + result = result.substr(prefix.size(), result.size() - prefix.size()); + } } + return result; +} - for (unsigned int i = 0; i < hex_str.length(); i += 2) { - std::string byteString = hex_str.substr(i, 2); - //if (byteString[0] == 0) byteString[0] = '0'; - //if (byteString[1] == 0) byteString[1] = '0'; - unsigned char byte = static_cast(strtol(byteString.c_str(), nullptr, 16)); - bytes.push_back(byte); +std::string_view utils::trimLeadingZeros(std::string_view src) +{ + std::string_view result = src; + while (result.size() && result[0] == '0') { + result = result.substr(1, result.size() - 1); } + return result; +} + +struct HexToU8Result { + bool success; + uint8_t u8; +}; + +static HexToU8Result hexToU8(char ch) { + HexToU8Result result = {}; + result.success = true; + + if (ch >= 'a' && ch <= 'f') + result.u8 = static_cast(ch - 'a' + 10); + else if (ch >= 'A' && ch <= 'F') + result.u8 = static_cast(ch - 'A' + 10); + else if (ch >= '0' && ch <= '9') + result.u8 = static_cast(ch - '0'); + else + result.success = false; - return bytes; + return result; } -uint64_t utils::fromHexStringToUint64(std::string hex_str) { - // Check for "0x" prefix and remove it - if(hex_str.size() >= 2 && hex_str[0] == '0' && hex_str[1] == 'x') { - hex_str = hex_str.substr(2); +std::vector utils::fromHexString(std::string_view hexStr) { + hexStr = trimPrefix(hexStr, "0x"); + assert(hexStr.size() % 2 == 0); + + std::vector result; + for (size_t i = 0; i < hexStr.length(); i += 2) { + std::string_view byteString = hexStr.substr(i, 2); + HexToU8Result hi = hexToU8(byteString[0]); + HexToU8Result lo = hexToU8(byteString[1]); + unsigned char byte = static_cast(hi.u8 << 4 | lo.u8 << 0); + result.push_back(byte); } + return result; +} - uint64_t value = std::stoull(hex_str, nullptr, 16); - return value; +uint64_t utils::fromHexStringToUint64(std::string_view hexStr) { + std::string_view realHex = trimPrefix(hexStr, "0x"); + + // NOTE: Trim leading '0's + while (realHex.size() && realHex[0] == '0') { + realHex = realHex.substr(1, realHex.size() - 1); + } + + size_t maxHexSize = sizeof(uint64_t) * 2 /*hex chars per byte*/; + assert(realHex.size() <= maxHexSize); + + size_t size = std::min(maxHexSize, realHex.size()); + uint64_t result = 0; + for (size_t index = 0; index < size; index++) { + char ch = realHex[index]; + HexToU8Result hexResult = hexToU8(ch); + assert(hexResult.success); + result = (result << 4) | hexResult.u8; + } + return result; } -std::array utils::fromHexString32Byte(std::string hex_str) { - std::vector bytesVec = fromHexString(hex_str); +std::array utils::fromHexString32Byte(std::string_view hexStr) { + std::vector bytesVec = fromHexString(hexStr); if(bytesVec.size() != 32) { throw std::invalid_argument("Input string length should be 64 characters for 32 bytes"); From 709f7c41bd8c62ab9fc26e72856d85ad822cbcda Mon Sep 17 00:00:00 2001 From: doylet Date: Tue, 16 Apr 2024 16:53:57 +1000 Subject: [PATCH 5/6] Improve error message when callReadFunction* fails --- src/provider.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/provider.cpp b/src/provider.cpp index aeac049..e5bbace 100644 --- a/src/provider.cpp +++ b/src/provider.cpp @@ -66,7 +66,13 @@ nlohmann::json Provider::callReadFunctionJSON(const ReadCallData& callData, std: } } - throw std::runtime_error("Unable to get the result of the function call"); + std::stringstream stream; + stream << "'eth_call' invoked on node for block '" << blockNumber + << "' to '" << callData.contractAddress + << "' with data payload '" << callData.data + << "' however it returned a response that does not have a result: " + << response.text; + throw std::runtime_error(stream.str()); } std::string Provider::callReadFunction(const ReadCallData& callData, std::string_view blockNumber) { From 7cde41d50dc94bc24040e98af1700ade310b6015 Mon Sep 17 00:00:00 2001 From: doylet Date: Wed, 17 Apr 2024 10:18:52 +1000 Subject: [PATCH 6/6] Update addessFromPrivateKey to secretKeyToAddressString in tests --- test/src/ethereum_client.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/src/ethereum_client.cpp b/test/src/ethereum_client.cpp index bf9a7e0..218b6cc 100644 --- a/test/src/ethereum_client.cpp +++ b/test/src/ethereum_client.cpp @@ -38,7 +38,7 @@ TEST_CASE( "SigningTest", "[signer]" ) { TEST_CASE( "Get address from private key", "[signer]" ) { std::vector seckey = utils::fromHexString(std::string(PRIVATE_KEY)); Signer signer; - std::string created_address = signer.addressFromPrivateKey(seckey); + std::string created_address = signer.secretKeyToAddressString(seckey); REQUIRE( created_address == ADDRESS ); }