Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updated Transaction::serialized and Signature::fromHex #13

Merged
merged 3 commits into from
Apr 26, 2024
Merged
Changes from 1 commit
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
125 changes: 73 additions & 52 deletions src/transaction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,52 +3,62 @@

#include <rlpvalue.h>
#include <iostream>
#include <exception>

/**
* Returns the raw Bytes of the EIP-1559 transaction, in order.
*
* Format: `[chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data,
* accessList, signatureYParity, signatureR, signatureS]`
*
* For an unsigned tx this method uses the empty Bytes values for the
* signature parameters `v`, `r` and `s` for encoding.
*/
* Serialize the transaction into a hexadecimal string using RLP encoding.
* This function handles both signed and unsigned transactions.
*
* Throws: std::runtime_error on failure to process any transaction component.
* Returns the raw Bytes of the EIP-1559 transaction, in order.
*
* Format: `[chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data,
* accessList, signatureYParity, signatureR, signatureS]`
*
* For an unsigned tx this method uses the empty Bytes values for the
* signature parameters `v`, `r` and `s` for encoding.
*/
std::string Transaction::serialized() const {
RLPValue arr(RLPValue::VARR);
arr.setArray();
RLPValue temp_val;
temp_val.clear();
temp_val.assign(utils::intToBytes(chainId));
arr.push_back(temp_val);
temp_val.assign(utils::intToBytes(nonce));
arr.push_back(temp_val);
temp_val.assign(utils::intToBytes(maxPriorityFeePerGas));
arr.push_back(temp_val);
temp_val.assign(utils::intToBytes(maxFeePerGas));
arr.push_back(temp_val);
temp_val.assign(utils::intToBytes(gasLimit));
arr.push_back(temp_val);
temp_val.assign(utils::fromHexString(to));
arr.push_back(temp_val);
temp_val.assign(utils::intToBytes(value));
arr.push_back(temp_val);
temp_val.assign(utils::fromHexString(data));
arr.push_back(temp_val);
try {
RLPValue arr(RLPValue::VARR);
arr.setArray();

// Access list not going to use
RLPValue access_list(RLPValue::VARR);
access_list.setArray();
arr.push_back(access_list);
// Helper lambda for repetitive operations to convert and append data
auto appendSerializedData = [&](const auto& data, auto conversionFunction) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

the data parameter here shadows the Transaction.data member. -Werror is set so this is causing the tests to fail atm

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry about that. I don't know why I didn't catch that.
I pushed a fix for that up to this branch and made a few more optimizations.
(compiler hint to inline the helper function, pre-allocation of memory, and some efficient conditional logic.)

Thinking about it now... Have you considered explicitly setting all transaction-related defaults in the constructor? This would ensure every transaction starts with a known good state.

Transaction(const std::string& _to, uint64_t _value, uint64_t _gasLimit = 21000, const std::string& _data = "", uint64_t _chainId = 1, uint64_t _nonce = 0, uint64_t _maxPriorityFeePerGas = 1, uint64_t _maxFeePerGas = 1)
    : chainId(_chainId), nonce(_nonce), maxPriorityFeePerGas(_maxPriorityFeePerGas), maxFeePerGas(_maxFeePerGas), to(_to), value(_value), gasLimit(_gasLimit), data(_data) {}

Copy link
Collaborator

Choose a reason for hiding this comment

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

The goal with the constructor was just to require the bare minimum needed to get a transaction going. I was assuming that most of the other items were going to be set by the providers defaults (ie id say its pretty rare that the user wants to manually set chainID but instead want to rely on the node to provide it), so putting them into a known bad state means that we can check if it has changed before we query the node.

I do like that the constructor at the moment is minimal, but it does assume that the default pathway will be for it to go through the signer/provider and things will be changed.

    Transaction tx("0x2Ccb8b65024E4aA9615a8E704DFb11BE76674f1F", 100000000000000);
    const auto hash = signer.sendTransaction(tx, seckey);

However I do see the point, tx above does not immediately come into a ready state. It would need extra steps like this

tx.chainId = 1;
tx.nonce = 1;

If you for example wanted the tx to be exported immediately without the signer/provider cleaning things up.

RLPValue temp_val;
try {
temp_val.assign(conversionFunction(data));
arr.push_back(temp_val);
} catch (const std::exception& e) {
throw std::runtime_error("Error converting transaction data: " + std::string(e.what()));
}
};

if (!sig.isEmpty()) {
temp_val.assign(utils::intToBytes(sig.signatureYParity));
arr.push_back(temp_val);
temp_val.assign(utils::removeLeadingZeros(sig.signatureR));
arr.push_back(temp_val);
temp_val.assign(utils::removeLeadingZeros(sig.signatureS));
arr.push_back(temp_val);
appendSerializedData(chainId, utils::intToBytes);
appendSerializedData(nonce, utils::intToBytes);
appendSerializedData(maxPriorityFeePerGas, utils::intToBytes);
appendSerializedData(maxFeePerGas, utils::intToBytes);
appendSerializedData(gasLimit, utils::intToBytes);
appendSerializedData(to, utils::fromHexString);
appendSerializedData(value, utils::intToBytes);
appendSerializedData(data, utils::fromHexString);

// Handle the access list, currently empty
RLPValue access_list(RLPValue::VARR);
access_list.setArray();
arr.push_back(access_list);

if (!sig.isEmpty()) {
appendSerializedData(sig.signatureYParity, utils::intToBytes);
appendSerializedData(sig.signatureR, utils::removeLeadingZeros);
appendSerializedData(sig.signatureS, utils::removeLeadingZeros);
}

return "0x02" + utils::toHexString(arr.write());
} catch (const std::exception& e) {
std::cerr << "Error serializing transaction: " << e.what() << std::endl;
throw;
}
return "0x02" + utils::toHexString(arr.write());
}

std::string Transaction::hash() const {
Expand All @@ -59,23 +69,34 @@ bool Signature::isEmpty() const {
return signatureYParity == 0 && signatureR.empty() && signatureS.empty();
};

/**
* Parses a signature from a hexadecimal string.
* The string should be exactly 130 characters long after removing the "0x" prefix.
*
* Parameters:
* - hex_str: Hexadecimal string containing the signature.
*
* Throws: std::invalid_argument if the input format is incorrect.
* Throws: std::runtime_error on parsing errors.
*/
void Signature::fromHex(std::string hex_str) {

// Check for "0x" prefix and remove it
if(hex_str.size() >= 2 && hex_str[0] == '0' && hex_str[1] == 'x') {
if (hex_str.size() >= 2 && hex_str.substr(0, 2) == "0x") {
hex_str = hex_str.substr(2);
}

if(hex_str.size() != 130) {
throw std::invalid_argument("Input string length should be 130 characters for 65 bytes");
if (hex_str.size() != 130) {
throw std::invalid_argument("Input string length should be 130 characters for 65 bytes (found " + std::to_string(hex_str.size()) + ")");
}

// Each byte is represented by 2 characters, so multiply indices by 2
std::string r_str = hex_str.substr(0, 64);
std::string s_str = hex_str.substr(64, 64);
std::string y_parity_str = hex_str.substr(128, 2);
try {
std::string r_str = hex_str.substr(0, 64);
std::string s_str = hex_str.substr(64, 64);
std::string y_parity_str = hex_str.substr(128, 2);

signatureR = utils::fromHexString(r_str);
signatureS = utils::fromHexString(s_str);
signatureYParity = std::stoull(y_parity_str, nullptr, 16);
signatureR = utils::fromHexString(r_str);
signatureS = utils::fromHexString(s_str);
signatureYParity = std::stoull(y_parity_str, nullptr, 16);
} catch (const std::exception& e) {
throw std::runtime_error("Error parsing hex string for signature: " + std::string(e.what()));
}
}
Loading