-
Notifications
You must be signed in to change notification settings - Fork 36
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
feat: add example for encoding and decoding raw transactions #164
Conversation
Hi @junha-ahn thanks for your PR! Please run For reader's flow we generally try to avoid helper function unless strictly necessary After the fix I need to double check if the proposed example reflects best practices |
examples/transactions/examples/encode_and_decode_raw_transaction.rs
Outdated
Show resolved
Hide resolved
fn build_unsigned_tx(chain_id: u64, to_address: Address) -> TxEip1559 { | ||
let mut tx = TxEip1559::default(); | ||
tx.chain_id = chain_id; | ||
tx.nonce = 0; | ||
tx.gas_limit = 21_000; | ||
tx.max_fee_per_gas = 20_000_000_000; | ||
tx.max_priority_fee_per_gas = 1_000_000_000; | ||
tx.to = TxKind::Call(to_address); // Change this to `TxKind::Create` if you'd like to deploy a contract instead | ||
tx.value = U256::from(100); | ||
|
||
tx | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can use the builder properties of TransactionRequest here
examples/examples/transactions/examples/send_eip1559_transaction.rs
Lines 22 to 31 in 5a6776f
// Build a transaction to send 100 wei from Alice to Bob. | |
// The `from` field is automatically filled to the first signer's address (Alice). | |
let tx = TransactionRequest::default() | |
.with_to(bob) | |
.with_nonce(0) | |
.with_chain_id(provider.get_chain_id().await?) | |
.with_value(U256::from(100)) | |
.with_gas_limit(21_000) | |
.with_max_priority_fee_per_gas(1_000_000_000) | |
.with_max_fee_per_gas(20_000_000_000); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I couldn't find the RLP-encoding-method in the TransactionRequest struct.
So Thats why I use TxEip1559.
however, I changed the coding style
fn build_unsigned_tx(chain_id: u64, to_address: Address) -> TxEip1559 {
TxEip1559 {
chain_id,
nonce: 0,
gas_limit: 21_000,
max_fee_per_gas: 20_000_000_000,
max_priority_fee_per_gas: 1_000_000_000,
to: TxKind::Call(to_address), // Change this to `TxKind::Create` if you'd like to deploy a contract instead
value: U256::from(100),
..Default::default()
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I couldn't find the RLP-encoding-method in the TransactionRequest struct.
there is no RLP-encoding method becasue TransactionRequest
cannot be RLP encoded. you should use build
to construct and sign a transaction, converting TransactionRequest
into TransactionEnvelope
fn unsigned_tx_to_bytes(tx: TxEip1559) -> Vec<u8> { | ||
tx.encoded_for_signing() // To use this, have to import "alloy::primitives::private::alloy_rlp::Encodable" | ||
} | ||
|
||
fn bytes_to_unsigned_tx(bytes: Vec<u8>) -> TxEip1559 { | ||
let mut slice = &bytes.as_slice()[1..]; | ||
TxEip1559::decode(&mut slice).unwrap() // To use this, have to import "alloy::primitives::private::alloy_rlp::Decodable" | ||
} | ||
|
||
async fn sign_tx(tx: TxEip1559, wallet: EthereumWallet) -> TxEnvelope { | ||
let tx_request: TransactionRequest = tx.into(); | ||
|
||
tx_request.build(&wallet).await.unwrap() | ||
} | ||
|
||
fn signed_tx_to_bytes(signed_tx: TxEnvelope) -> Vec<u8> { | ||
let mut encoded = Vec::new(); | ||
signed_tx.encode(&mut encoded); | ||
let encoded = &encoded[2..]; | ||
encoded.into() | ||
} | ||
|
||
fn bytes_to_signed_tx(bytes: Vec<u8>) -> TxEnvelope { | ||
let mut slice = bytes.as_slice(); | ||
TxEnvelope::decode(&mut slice).unwrap() | ||
} | ||
|
||
fn get_tx_hash_from_signed_tx_bytes(signed_tx_bytes: Vec<u8>) -> FixedBytes<32> { | ||
format!("0x{}", hex::encode(keccak256(signed_tx_bytes.clone()))) | ||
.parse::<FixedBytes<32>>() | ||
.unwrap() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this can all be inlined for clarity, using ?
with eyre for conciseness
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fn bytes_to_unsigned_tx(bytes: Vec<u8>) -> eyre::Result<TxEip1559> {
let mut slice = &bytes.as_slice()[1..];
Ok(TxEip1559::decode(&mut slice)?) // To use this, have to import "alloy::primitives::private::alloy_rlp::Decodable"
}
If I will change like above, then I have to handle all Result enum in the main function
I think, its not conciseness
examples/transactions/examples/encode_and_decode_raw_transaction.rs
Outdated
Show resolved
Hide resolved
From #86
cc @prestwich / @yash-atreya would you mind double checking this example if this is in line with the Alloy API? If not, how should it be handled differently? I want to make sure that the examples reflect how we intend the API to be used. |
this example seems to be the same thing, but more concise and more in line with intended API usage |
I am currently developing two servers, both using alloy:
Using these two servers, I have the following transaction lifecycle:
build/broadcast transaction server <= rlp bytes => sign only server The send_eip1559_transaction example doesn't use RLP bytes at all, so it's not the same as the PR's example. The PR's example focuses on using RLP. Additional note1: While each server could communicate via JSON requests, the sign server is designed to support requests from all networks, not just EVM but also Solana and others. Therefore, I believe it's a more efficient design to exchange raw transactions rather than parsing specific JSON bodies for each network. Additional note2: Currently, it seems like they're trying to hide RLP Encoding in the Alloy design (I had a really hard time because of this. I spent hours searching through various methods). I'm curious why users shouldn't handle RLP encoding directly. For use cases like mine, should I not be using Alloy? |
the API was not designed to support blind signing setups like this, as they are considered not best practices. To be precise, you do not actually want RLP bytes. Signing RLP will cause txns to be rejected by the network, as will broadcasting signed RLP. You want transaction-specific unsigned formats, which generally-but-not-always use RLP as a substring. Please be careful when implementing these sorts of things, as it is very easy to miss a detail and end up with subtly incorrect code (for an example, see this longstanding bug in the ledger hardware wallet app) . This is why we don't recommend users interact with RLP directly, and do not generally want examples of doing so |
Your core problem is that there is no official format serialization format for unsigned transactions except the JSON-RPC You might find the hidden |
Due to my incomplete understanding of a large amount of Alloy code, there might be errors in each of my opinions.
|
As I said above, your core problem is that there is no official format serialization format for unsigned transactions except the JSON-RPC
No,
Yes, that is intentional, as doing so would never be a best practice, as it is not an industry standard. As a side note, even signed transaction transmission via RLP is non-standard. EIP-2718 is the only standard binary format for serialized signed transactions
Server separation and isolation is great. |
I understand what the approach is aiming for. I have one question. Until now, I've been using RLP serialization as a matter of course while using SDKs in the Node.js ecosystem. I wasn't aware that JSON serialization was the best practice and RLP serialization was similar to being deprecated(?). Are there any official discussions or links about this topic that you could share? (Since this is the first time I'm hearing about this, I'm curious where I should look for references - for future server design discussions and suggestions) |
it's not that RLP is being deprecated, it's that it is an implementation detail of the network's EIP-2718 specification for each transaction type, (and therefore an implementation detail of consensus). The EIP-2718 specification is used by (e.g.) This means there is no well-specified, standardized RLP or 2718 encoding of unsigned transactions. Transmitting unsigned transactions at all is generally not standardized at all. So when writing a library, if you provide those methods you are likely pushing your users into behavior that is not compatible with other tooling in the ecosystem Most signers (like metamask or full node wallets, etc) tend to use the JSON-RPC transaction request, while special-application signers like ledger, hsms, etc, design custom serializations. Some signers, like the GCP blind signer we have in alloy transmit only the hash to be signed (which is less-than-ideal 😮💨 ) |
@prestwich Is there any recommended way to rlp encode signed tx? I currently use this way fn signed_tx_to_bytes(signed_tx: TxEnvelope) -> Vec<u8> {
let mut encoded = Vec::new();
signed_tx.encode(&mut encoded);
let encoded = &encoded[2..]; // I don't know why do we have to slice 2 bytes.
encoded.into() // the result is "raw transaction" that you can see in the etherscan
} |
I would strongly recommend using |
i think you just need to import the Encodable2718 trait :) |
@prestwich I apologize for the continuous questions in this thread. I have a question about signing. Currently, it seems that to sign a If I want to use the // fn signature
fn sign_transaction_sync(
&self,
tx: &mut dyn SignableTransaction<Signature>,
) -> alloy_signer::Result<Signature>;
// use like below:
let mut tx: alloy::consensus::TxEip1559 = ...;
let signature = signer.sign_transaction_sync(&mut tx).unwrap();
let signed_tx: TxEnvelope = tx.into_signed(signature).into(); I would need to convert TransactionRequest to TxEip1559 (SignableTransaction).
Does
|
This is what the
I think so right now, but adding a |
Motivation
This PR adds an example demonstrating how to encode, decode, sign, and send raw transactions using the Alloy library.
It shows the complete lifecycle of an EVM transaction.
This example will help developers understand how to work with raw transactions in Alloy.
Solution
the code creates a transaction, converts it to bytes, then back to a transaction object, verifying each step with assertions to ensure data integrity through the encoding/decoding process.
PR Checklist