mirror of
https://github.com/hl-archive-node/nanoreth.git
synced 2025-12-06 10:59:55 +00:00
1809 lines
71 KiB
Rust
1809 lines
71 KiB
Rust
use crate::{
|
|
compression::{TRANSACTION_COMPRESSOR, TRANSACTION_DECOMPRESSOR},
|
|
keccak256, Address, BlockHashOrNumber, Bytes, TxHash, B256,
|
|
};
|
|
use alloy_rlp::{
|
|
Decodable, Encodable, Error as RlpError, Header, EMPTY_LIST_CODE, EMPTY_STRING_CODE,
|
|
};
|
|
use bytes::{Buf, BytesMut};
|
|
use derive_more::{AsRef, Deref};
|
|
use once_cell::sync::Lazy;
|
|
use rayon::prelude::{IntoParallelIterator, ParallelIterator};
|
|
use reth_codecs::{add_arbitrary_tests, derive_arbitrary, Compact};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::mem;
|
|
|
|
pub use access_list::{AccessList, AccessListItem};
|
|
pub use eip1559::TxEip1559;
|
|
pub use eip2930::TxEip2930;
|
|
pub use eip4844::TxEip4844;
|
|
|
|
pub use error::InvalidTransactionError;
|
|
pub use legacy::TxLegacy;
|
|
pub use meta::TransactionMeta;
|
|
#[cfg(feature = "c-kzg")]
|
|
pub use pooled::{PooledTransactionsElement, PooledTransactionsElementEcRecovered};
|
|
#[cfg(feature = "c-kzg")]
|
|
pub use sidecar::{BlobTransaction, BlobTransactionSidecar, BlobTransactionValidationError};
|
|
pub use signature::Signature;
|
|
pub use tx_type::{
|
|
TxType, EIP1559_TX_TYPE_ID, EIP2930_TX_TYPE_ID, EIP4844_TX_TYPE_ID, LEGACY_TX_TYPE_ID,
|
|
};
|
|
pub use tx_value::TxValue;
|
|
pub use variant::TransactionSignedVariant;
|
|
|
|
mod access_list;
|
|
mod eip1559;
|
|
mod eip2930;
|
|
mod eip4844;
|
|
mod error;
|
|
mod legacy;
|
|
mod meta;
|
|
#[cfg(feature = "c-kzg")]
|
|
mod pooled;
|
|
#[cfg(feature = "c-kzg")]
|
|
mod sidecar;
|
|
mod signature;
|
|
mod tx_type;
|
|
mod tx_value;
|
|
pub(crate) mod util;
|
|
mod variant;
|
|
|
|
#[cfg(feature = "optimism")]
|
|
mod optimism;
|
|
#[cfg(feature = "optimism")]
|
|
pub use optimism::TxDeposit;
|
|
#[cfg(feature = "optimism")]
|
|
pub use tx_type::DEPOSIT_TX_TYPE_ID;
|
|
|
|
// Expected number of transactions where we can expect a speed-up by recovering the senders in
|
|
// parallel.
|
|
pub(crate) static PARALLEL_SENDER_RECOVERY_THRESHOLD: Lazy<usize> =
|
|
Lazy::new(|| match rayon::current_num_threads() {
|
|
0..=1 => usize::MAX,
|
|
2..=8 => 10,
|
|
_ => 5,
|
|
});
|
|
|
|
/// A raw transaction.
|
|
///
|
|
/// Transaction types were introduced in [EIP-2718](https://eips.ethereum.org/EIPS/eip-2718).
|
|
#[derive_arbitrary(compact)]
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
pub enum Transaction {
|
|
/// Legacy transaction (type `0x0`).
|
|
///
|
|
/// Traditional Ethereum transactions, containing parameters `nonce`, `gasPrice`, `gasLimit`,
|
|
/// `to`, `value`, `data`, `v`, `r`, and `s`.
|
|
///
|
|
/// These transactions do not utilize access lists nor do they incorporate EIP-1559 fee market
|
|
/// changes.
|
|
Legacy(TxLegacy),
|
|
/// Transaction with an [`AccessList`] ([EIP-2930](https://eips.ethereum.org/EIPS/eip-2930)), type `0x1`.
|
|
///
|
|
/// The `accessList` specifies an array of addresses and storage keys that the transaction
|
|
/// plans to access, enabling gas savings on cross-contract calls by pre-declaring the accessed
|
|
/// contract and storage slots.
|
|
Eip2930(TxEip2930),
|
|
/// A transaction with a priority fee ([EIP-1559](https://eips.ethereum.org/EIPS/eip-1559)), type `0x2`.
|
|
///
|
|
/// Unlike traditional transactions, EIP-1559 transactions use an in-protocol, dynamically
|
|
/// changing base fee per gas, adjusted at each block to manage network congestion.
|
|
///
|
|
/// - `maxPriorityFeePerGas`, specifying the maximum fee above the base fee the sender is
|
|
/// willing to pay
|
|
/// - `maxFeePerGas`, setting the maximum total fee the sender is willing to pay.
|
|
///
|
|
/// The base fee is burned, while the priority fee is paid to the miner who includes the
|
|
/// transaction, incentivizing miners to include transactions with higher priority fees per
|
|
/// gas.
|
|
Eip1559(TxEip1559),
|
|
/// Shard Blob Transactions ([EIP-4844](https://eips.ethereum.org/EIPS/eip-4844)), type `0x3`.
|
|
///
|
|
/// Shard Blob Transactions introduce a new transaction type called a blob-carrying transaction
|
|
/// to reduce gas costs. These transactions are similar to regular Ethereum transactions but
|
|
/// include additional data called a blob.
|
|
///
|
|
/// Blobs are larger (~125 kB) and cheaper than the current calldata, providing an immutable
|
|
/// and read-only memory for storing transaction data.
|
|
///
|
|
/// EIP-4844, also known as proto-danksharding, implements the framework and logic of
|
|
/// danksharding, introducing new transaction formats and verification rules.
|
|
Eip4844(TxEip4844),
|
|
/// Optimism deposit transaction.
|
|
#[cfg(feature = "optimism")]
|
|
Deposit(TxDeposit),
|
|
}
|
|
|
|
// === impl Transaction ===
|
|
|
|
impl Transaction {
|
|
/// Heavy operation that return signature hash over rlp encoded transaction.
|
|
/// It is only for signature signing or signer recovery.
|
|
pub fn signature_hash(&self) -> B256 {
|
|
match self {
|
|
Transaction::Legacy(tx) => tx.signature_hash(),
|
|
Transaction::Eip2930(tx) => tx.signature_hash(),
|
|
Transaction::Eip1559(tx) => tx.signature_hash(),
|
|
Transaction::Eip4844(tx) => tx.signature_hash(),
|
|
#[cfg(feature = "optimism")]
|
|
Transaction::Deposit(_) => B256::ZERO,
|
|
}
|
|
}
|
|
|
|
/// Get chain_id.
|
|
pub fn chain_id(&self) -> Option<u64> {
|
|
match self {
|
|
Transaction::Legacy(TxLegacy { chain_id, .. }) => *chain_id,
|
|
Transaction::Eip2930(TxEip2930 { chain_id, .. }) |
|
|
Transaction::Eip1559(TxEip1559 { chain_id, .. }) |
|
|
Transaction::Eip4844(TxEip4844 { chain_id, .. }) => Some(*chain_id),
|
|
#[cfg(feature = "optimism")]
|
|
Transaction::Deposit(_) => None,
|
|
}
|
|
}
|
|
|
|
/// Sets the transaction's chain id to the provided value.
|
|
pub fn set_chain_id(&mut self, chain_id: u64) {
|
|
match self {
|
|
Transaction::Legacy(TxLegacy { chain_id: ref mut c, .. }) => *c = Some(chain_id),
|
|
Transaction::Eip2930(TxEip2930 { chain_id: ref mut c, .. }) |
|
|
Transaction::Eip1559(TxEip1559 { chain_id: ref mut c, .. }) |
|
|
Transaction::Eip4844(TxEip4844 { chain_id: ref mut c, .. }) => *c = chain_id,
|
|
#[cfg(feature = "optimism")]
|
|
Transaction::Deposit(_) => { /* noop */ }
|
|
}
|
|
}
|
|
|
|
/// Gets the transaction's [`TransactionKind`], which is the address of the recipient or
|
|
/// [`TransactionKind::Create`] if the transaction is a contract creation.
|
|
pub fn kind(&self) -> &TransactionKind {
|
|
match self {
|
|
Transaction::Legacy(TxLegacy { to, .. }) |
|
|
Transaction::Eip2930(TxEip2930 { to, .. }) |
|
|
Transaction::Eip1559(TxEip1559 { to, .. }) |
|
|
Transaction::Eip4844(TxEip4844 { to, .. }) => to,
|
|
#[cfg(feature = "optimism")]
|
|
Transaction::Deposit(TxDeposit { to, .. }) => to,
|
|
}
|
|
}
|
|
|
|
/// Get the transaction's nonce.
|
|
pub fn to(&self) -> Option<Address> {
|
|
self.kind().to()
|
|
}
|
|
|
|
/// Get transaction type
|
|
pub fn tx_type(&self) -> TxType {
|
|
match self {
|
|
Transaction::Legacy(legacy_tx) => legacy_tx.tx_type(),
|
|
Transaction::Eip2930(access_list_tx) => access_list_tx.tx_type(),
|
|
Transaction::Eip1559(dynamic_fee_tx) => dynamic_fee_tx.tx_type(),
|
|
Transaction::Eip4844(blob_tx) => blob_tx.tx_type(),
|
|
#[cfg(feature = "optimism")]
|
|
Transaction::Deposit(deposit_tx) => deposit_tx.tx_type(),
|
|
}
|
|
}
|
|
|
|
/// Gets the transaction's value field.
|
|
pub fn value(&self) -> TxValue {
|
|
*match self {
|
|
Transaction::Legacy(TxLegacy { value, .. }) |
|
|
Transaction::Eip2930(TxEip2930 { value, .. }) |
|
|
Transaction::Eip1559(TxEip1559 { value, .. }) |
|
|
Transaction::Eip4844(TxEip4844 { value, .. }) => value,
|
|
#[cfg(feature = "optimism")]
|
|
Transaction::Deposit(TxDeposit { value, .. }) => value,
|
|
}
|
|
}
|
|
|
|
/// Get the transaction's nonce.
|
|
pub fn nonce(&self) -> u64 {
|
|
match self {
|
|
Transaction::Legacy(TxLegacy { nonce, .. }) |
|
|
Transaction::Eip2930(TxEip2930 { nonce, .. }) |
|
|
Transaction::Eip1559(TxEip1559 { nonce, .. }) |
|
|
Transaction::Eip4844(TxEip4844 { nonce, .. }) => *nonce,
|
|
// Deposit transactions do not have nonces.
|
|
#[cfg(feature = "optimism")]
|
|
Transaction::Deposit(_) => 0,
|
|
}
|
|
}
|
|
|
|
/// Returns the [AccessList] of the transaction.
|
|
///
|
|
/// Returns `None` for legacy transactions.
|
|
pub fn access_list(&self) -> Option<&AccessList> {
|
|
match self {
|
|
Transaction::Legacy(_) => None,
|
|
Transaction::Eip2930(tx) => Some(&tx.access_list),
|
|
Transaction::Eip1559(tx) => Some(&tx.access_list),
|
|
Transaction::Eip4844(tx) => Some(&tx.access_list),
|
|
#[cfg(feature = "optimism")]
|
|
Transaction::Deposit(_) => None,
|
|
}
|
|
}
|
|
|
|
/// Get the gas limit of the transaction.
|
|
pub fn gas_limit(&self) -> u64 {
|
|
match self {
|
|
Transaction::Legacy(TxLegacy { gas_limit, .. }) |
|
|
Transaction::Eip2930(TxEip2930 { gas_limit, .. }) |
|
|
Transaction::Eip1559(TxEip1559 { gas_limit, .. }) |
|
|
Transaction::Eip4844(TxEip4844 { gas_limit, .. }) => *gas_limit,
|
|
#[cfg(feature = "optimism")]
|
|
Transaction::Deposit(TxDeposit { gas_limit, .. }) => *gas_limit,
|
|
}
|
|
}
|
|
|
|
/// Returns true if the tx supports dynamic fees
|
|
pub fn is_dynamic_fee(&self) -> bool {
|
|
match self {
|
|
Transaction::Legacy(_) | Transaction::Eip2930(_) => false,
|
|
Transaction::Eip1559(_) | Transaction::Eip4844(_) => true,
|
|
#[cfg(feature = "optimism")]
|
|
Transaction::Deposit(_) => false,
|
|
}
|
|
}
|
|
|
|
/// Max fee per gas for eip1559 transaction, for legacy transactions this is gas_price.
|
|
///
|
|
/// This is also commonly referred to as the "Gas Fee Cap" (`GasFeeCap`).
|
|
pub fn max_fee_per_gas(&self) -> u128 {
|
|
match self {
|
|
Transaction::Legacy(TxLegacy { gas_price, .. }) |
|
|
Transaction::Eip2930(TxEip2930 { gas_price, .. }) => *gas_price,
|
|
Transaction::Eip1559(TxEip1559 { max_fee_per_gas, .. }) |
|
|
Transaction::Eip4844(TxEip4844 { max_fee_per_gas, .. }) => *max_fee_per_gas,
|
|
// Deposit transactions buy their L2 gas on L1 and, as such, the L2 gas is not
|
|
// refundable.
|
|
#[cfg(feature = "optimism")]
|
|
Transaction::Deposit(_) => 0,
|
|
}
|
|
}
|
|
|
|
/// Max priority fee per gas for eip1559 transaction, for legacy and eip2930 transactions this
|
|
/// is `None`
|
|
///
|
|
/// This is also commonly referred to as the "Gas Tip Cap" (`GasTipCap`).
|
|
pub fn max_priority_fee_per_gas(&self) -> Option<u128> {
|
|
match self {
|
|
Transaction::Legacy(_) | Transaction::Eip2930(_) => None,
|
|
Transaction::Eip1559(TxEip1559 { max_priority_fee_per_gas, .. }) |
|
|
Transaction::Eip4844(TxEip4844 { max_priority_fee_per_gas, .. }) => {
|
|
Some(*max_priority_fee_per_gas)
|
|
}
|
|
#[cfg(feature = "optimism")]
|
|
Transaction::Deposit(_) => None,
|
|
}
|
|
}
|
|
|
|
/// Blob versioned hashes for eip4844 transaction, for legacy,eip1559 and eip2930 transactions
|
|
/// this is `None`
|
|
///
|
|
/// This is also commonly referred to as the "blob versioned hashes" (`BlobVersionedHashes`).
|
|
pub fn blob_versioned_hashes(&self) -> Option<Vec<B256>> {
|
|
match self {
|
|
Transaction::Legacy(_) | Transaction::Eip2930(_) | Transaction::Eip1559(_) => None,
|
|
Transaction::Eip4844(TxEip4844 { blob_versioned_hashes, .. }) => {
|
|
Some(blob_versioned_hashes.to_vec())
|
|
}
|
|
#[cfg(feature = "optimism")]
|
|
Transaction::Deposit(_) => None,
|
|
}
|
|
}
|
|
|
|
/// Max fee per blob gas for eip4844 transaction [TxEip4844].
|
|
///
|
|
/// Returns `None` for non-eip4844 transactions.
|
|
///
|
|
/// This is also commonly referred to as the "Blob Gas Fee Cap" (`BlobGasFeeCap`).
|
|
pub fn max_fee_per_blob_gas(&self) -> Option<u128> {
|
|
match self {
|
|
Transaction::Eip4844(TxEip4844 { max_fee_per_blob_gas, .. }) => {
|
|
Some(*max_fee_per_blob_gas)
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Returns the blob gas used for all blobs of the EIP-4844 transaction if it is an EIP-4844
|
|
/// transaction.
|
|
///
|
|
/// This is the number of blobs times the
|
|
/// [DATA_GAS_PER_BLOB](crate::constants::eip4844::DATA_GAS_PER_BLOB) a single blob consumes.
|
|
pub fn blob_gas_used(&self) -> Option<u64> {
|
|
self.as_eip4844().map(TxEip4844::blob_gas)
|
|
}
|
|
|
|
/// Return the max priority fee per gas if the transaction is an EIP-1559 transaction, and
|
|
/// otherwise return the gas price.
|
|
///
|
|
/// # Warning
|
|
///
|
|
/// This is different than the `max_priority_fee_per_gas` method, which returns `None` for
|
|
/// non-EIP-1559 transactions.
|
|
pub fn priority_fee_or_price(&self) -> u128 {
|
|
match self {
|
|
Transaction::Legacy(TxLegacy { gas_price, .. }) |
|
|
Transaction::Eip2930(TxEip2930 { gas_price, .. }) => *gas_price,
|
|
Transaction::Eip1559(TxEip1559 { max_priority_fee_per_gas, .. }) |
|
|
Transaction::Eip4844(TxEip4844 { max_priority_fee_per_gas, .. }) => {
|
|
*max_priority_fee_per_gas
|
|
}
|
|
#[cfg(feature = "optimism")]
|
|
Transaction::Deposit(_) => 0,
|
|
}
|
|
}
|
|
|
|
/// Returns the effective gas price for the given base fee.
|
|
///
|
|
/// If the transaction is a legacy or EIP2930 transaction, the gas price is returned.
|
|
pub fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
|
|
match self {
|
|
Transaction::Legacy(tx) => tx.gas_price,
|
|
Transaction::Eip2930(tx) => tx.gas_price,
|
|
Transaction::Eip1559(dynamic_tx) => dynamic_tx.effective_gas_price(base_fee),
|
|
Transaction::Eip4844(dynamic_tx) => dynamic_tx.effective_gas_price(base_fee),
|
|
#[cfg(feature = "optimism")]
|
|
Transaction::Deposit(_) => 0,
|
|
}
|
|
}
|
|
|
|
/// Returns the effective miner gas tip cap (`gasTipCap`) for the given base fee:
|
|
/// `min(maxFeePerGas - baseFee, maxPriorityFeePerGas)`
|
|
///
|
|
/// If the base fee is `None`, the `max_priority_fee_per_gas`, or gas price for non-EIP1559
|
|
/// transactions is returned.
|
|
///
|
|
/// Returns `None` if the basefee is higher than the [Transaction::max_fee_per_gas].
|
|
pub fn effective_tip_per_gas(&self, base_fee: Option<u64>) -> Option<u128> {
|
|
let base_fee = match base_fee {
|
|
Some(base_fee) => base_fee as u128,
|
|
None => return Some(self.priority_fee_or_price()),
|
|
};
|
|
|
|
let max_fee_per_gas = self.max_fee_per_gas();
|
|
|
|
// Check if max_fee_per_gas is less than base_fee
|
|
if max_fee_per_gas < base_fee {
|
|
return None
|
|
}
|
|
|
|
// Calculate the difference between max_fee_per_gas and base_fee
|
|
let fee = max_fee_per_gas - base_fee;
|
|
|
|
// Compare the fee with max_priority_fee_per_gas (or gas price for non-EIP1559 transactions)
|
|
if let Some(priority_fee) = self.max_priority_fee_per_gas() {
|
|
Some(fee.min(priority_fee))
|
|
} else {
|
|
Some(fee)
|
|
}
|
|
}
|
|
|
|
/// Get the transaction's input field.
|
|
pub fn input(&self) -> &Bytes {
|
|
match self {
|
|
Transaction::Legacy(TxLegacy { input, .. }) |
|
|
Transaction::Eip2930(TxEip2930 { input, .. }) |
|
|
Transaction::Eip1559(TxEip1559 { input, .. }) |
|
|
Transaction::Eip4844(TxEip4844 { input, .. }) => input,
|
|
#[cfg(feature = "optimism")]
|
|
Transaction::Deposit(TxDeposit { input, .. }) => input,
|
|
}
|
|
}
|
|
|
|
/// Returns the source hash of the transaction, which uniquely identifies its source.
|
|
/// If not a deposit transaction, this will always return `None`.
|
|
#[cfg(feature = "optimism")]
|
|
pub fn source_hash(&self) -> Option<B256> {
|
|
match self {
|
|
Transaction::Deposit(TxDeposit { source_hash, .. }) => Some(*source_hash),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Returns the amount of ETH locked up on L1 that will be minted on L2. If the transaction
|
|
/// is not a deposit transaction, this will always return `None`.
|
|
#[cfg(feature = "optimism")]
|
|
pub fn mint(&self) -> Option<u128> {
|
|
match self {
|
|
Transaction::Deposit(TxDeposit { mint, .. }) => *mint,
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Returns whether or not the transaction is a system transaction. If the transaction
|
|
/// is not a deposit transaction, this will always return `false`.
|
|
#[cfg(feature = "optimism")]
|
|
pub fn is_system_transaction(&self) -> bool {
|
|
match self {
|
|
Transaction::Deposit(TxDeposit { is_system_transaction, .. }) => *is_system_transaction,
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
/// Returns whether or not the transaction is an Optimism Deposited transaction.
|
|
#[cfg(feature = "optimism")]
|
|
pub fn is_deposit(&self) -> bool {
|
|
matches!(self, Transaction::Deposit(_))
|
|
}
|
|
|
|
/// This encodes the transaction _without_ the signature, and is only suitable for creating a
|
|
/// hash intended for signing.
|
|
pub fn encode_without_signature(&self, out: &mut dyn bytes::BufMut) {
|
|
Encodable::encode(self, out);
|
|
}
|
|
|
|
/// Inner encoding function that is used for both rlp [`Encodable`] trait and for calculating
|
|
/// hash that for eip2718 does not require rlp header
|
|
pub fn encode_with_signature(
|
|
&self,
|
|
signature: &Signature,
|
|
out: &mut dyn bytes::BufMut,
|
|
with_header: bool,
|
|
) {
|
|
match self {
|
|
Transaction::Legacy(legacy_tx) => {
|
|
// do nothing w/ with_header
|
|
legacy_tx.encode_with_signature(signature, out)
|
|
}
|
|
Transaction::Eip2930(access_list_tx) => {
|
|
access_list_tx.encode_with_signature(signature, out, with_header)
|
|
}
|
|
Transaction::Eip1559(dynamic_fee_tx) => {
|
|
dynamic_fee_tx.encode_with_signature(signature, out, with_header)
|
|
}
|
|
Transaction::Eip4844(blob_tx) => {
|
|
blob_tx.encode_with_signature(signature, out, with_header)
|
|
}
|
|
#[cfg(feature = "optimism")]
|
|
Transaction::Deposit(deposit_tx) => deposit_tx.encode(out, with_header),
|
|
}
|
|
}
|
|
|
|
/// This sets the transaction's nonce.
|
|
pub fn set_nonce(&mut self, nonce: u64) {
|
|
match self {
|
|
Transaction::Legacy(tx) => tx.nonce = nonce,
|
|
Transaction::Eip2930(tx) => tx.nonce = nonce,
|
|
Transaction::Eip1559(tx) => tx.nonce = nonce,
|
|
Transaction::Eip4844(tx) => tx.nonce = nonce,
|
|
#[cfg(feature = "optimism")]
|
|
Transaction::Deposit(_) => { /* noop */ }
|
|
}
|
|
}
|
|
|
|
/// This sets the transaction's value.
|
|
pub fn set_value(&mut self, value: TxValue) {
|
|
match self {
|
|
Transaction::Legacy(tx) => tx.value = value,
|
|
Transaction::Eip2930(tx) => tx.value = value,
|
|
Transaction::Eip1559(tx) => tx.value = value,
|
|
Transaction::Eip4844(tx) => tx.value = value,
|
|
#[cfg(feature = "optimism")]
|
|
Transaction::Deposit(tx) => tx.value = value,
|
|
}
|
|
}
|
|
|
|
/// This sets the transaction's input field.
|
|
pub fn set_input(&mut self, input: Bytes) {
|
|
match self {
|
|
Transaction::Legacy(tx) => tx.input = input,
|
|
Transaction::Eip2930(tx) => tx.input = input,
|
|
Transaction::Eip1559(tx) => tx.input = input,
|
|
Transaction::Eip4844(tx) => tx.input = input,
|
|
#[cfg(feature = "optimism")]
|
|
Transaction::Deposit(tx) => tx.input = input,
|
|
}
|
|
}
|
|
|
|
/// Calculates a heuristic for the in-memory size of the [Transaction].
|
|
#[inline]
|
|
fn size(&self) -> usize {
|
|
match self {
|
|
Transaction::Legacy(tx) => tx.size(),
|
|
Transaction::Eip2930(tx) => tx.size(),
|
|
Transaction::Eip1559(tx) => tx.size(),
|
|
Transaction::Eip4844(tx) => tx.size(),
|
|
#[cfg(feature = "optimism")]
|
|
Transaction::Deposit(tx) => tx.size(),
|
|
}
|
|
}
|
|
|
|
/// Returns true if the transaction is a legacy transaction.
|
|
#[inline]
|
|
pub fn is_legacy(&self) -> bool {
|
|
matches!(self, Transaction::Legacy(_))
|
|
}
|
|
|
|
/// Returns true if the transaction is an EIP-2930 transaction.
|
|
#[inline]
|
|
pub fn is_eip2930(&self) -> bool {
|
|
matches!(self, Transaction::Eip2930(_))
|
|
}
|
|
|
|
/// Returns true if the transaction is an EIP-1559 transaction.
|
|
#[inline]
|
|
pub fn is_eip1559(&self) -> bool {
|
|
matches!(self, Transaction::Eip1559(_))
|
|
}
|
|
|
|
/// Returns true if the transaction is an EIP-4844 transaction.
|
|
#[inline]
|
|
pub fn is_eip4844(&self) -> bool {
|
|
matches!(self, Transaction::Eip4844(_))
|
|
}
|
|
|
|
/// Returns the [TxLegacy] variant if the transaction is a legacy transaction.
|
|
pub fn as_legacy(&self) -> Option<&TxLegacy> {
|
|
match self {
|
|
Transaction::Legacy(tx) => Some(tx),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Returns the [TxEip2930] variant if the transaction is an EIP-2930 transaction.
|
|
pub fn as_eip2930(&self) -> Option<&TxEip2930> {
|
|
match self {
|
|
Transaction::Eip2930(tx) => Some(tx),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Returns the [TxEip1559] variant if the transaction is an EIP-1559 transaction.
|
|
pub fn as_eip1559(&self) -> Option<&TxEip1559> {
|
|
match self {
|
|
Transaction::Eip1559(tx) => Some(tx),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Returns the [TxEip4844] variant if the transaction is an EIP-4844 transaction.
|
|
pub fn as_eip4844(&self) -> Option<&TxEip4844> {
|
|
match self {
|
|
Transaction::Eip4844(tx) => Some(tx),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<TxLegacy> for Transaction {
|
|
fn from(tx: TxLegacy) -> Self {
|
|
Transaction::Legacy(tx)
|
|
}
|
|
}
|
|
|
|
impl From<TxEip2930> for Transaction {
|
|
fn from(tx: TxEip2930) -> Self {
|
|
Transaction::Eip2930(tx)
|
|
}
|
|
}
|
|
|
|
impl From<TxEip1559> for Transaction {
|
|
fn from(tx: TxEip1559) -> Self {
|
|
Transaction::Eip1559(tx)
|
|
}
|
|
}
|
|
|
|
impl From<TxEip4844> for Transaction {
|
|
fn from(tx: TxEip4844) -> Self {
|
|
Transaction::Eip4844(tx)
|
|
}
|
|
}
|
|
|
|
impl Compact for Transaction {
|
|
// Serializes the TxType to the buffer if necessary, returning 2 bits of the type as an
|
|
// identifier instead of the length.
|
|
fn to_compact<B>(self, buf: &mut B) -> usize
|
|
where
|
|
B: bytes::BufMut + AsMut<[u8]>,
|
|
{
|
|
let identifier = self.tx_type().to_compact(buf);
|
|
match self {
|
|
Transaction::Legacy(tx) => {
|
|
tx.to_compact(buf);
|
|
}
|
|
Transaction::Eip2930(tx) => {
|
|
tx.to_compact(buf);
|
|
}
|
|
Transaction::Eip1559(tx) => {
|
|
tx.to_compact(buf);
|
|
}
|
|
Transaction::Eip4844(tx) => {
|
|
tx.to_compact(buf);
|
|
}
|
|
#[cfg(feature = "optimism")]
|
|
Transaction::Deposit(tx) => {
|
|
tx.to_compact(buf);
|
|
}
|
|
}
|
|
identifier
|
|
}
|
|
|
|
// For backwards compatibility purposes, only 2 bits of the type are encoded in the identifier
|
|
// parameter. In the case of a 3, the full transaction type is read from the buffer as a
|
|
// single byte.
|
|
fn from_compact(mut buf: &[u8], identifier: usize) -> (Self, &[u8]) {
|
|
match identifier {
|
|
0 => {
|
|
let (tx, buf) = TxLegacy::from_compact(buf, buf.len());
|
|
(Transaction::Legacy(tx), buf)
|
|
}
|
|
1 => {
|
|
let (tx, buf) = TxEip2930::from_compact(buf, buf.len());
|
|
(Transaction::Eip2930(tx), buf)
|
|
}
|
|
2 => {
|
|
let (tx, buf) = TxEip1559::from_compact(buf, buf.len());
|
|
(Transaction::Eip1559(tx), buf)
|
|
}
|
|
3 => {
|
|
// An identifier of 3 indicates that the transaction type did not fit into
|
|
// the backwards compatible 2 bit identifier, their transaction types are
|
|
// larger than 2 bits (eg. 4844 and Deposit Transactions). In this case,
|
|
// we need to read the concrete transaction type from the buffer by
|
|
// reading the full 8 bits (single byte) and match on this transaction type.
|
|
let identifier = buf.get_u8() as usize;
|
|
match identifier {
|
|
3 => {
|
|
let (tx, buf) = TxEip4844::from_compact(buf, buf.len());
|
|
(Transaction::Eip4844(tx), buf)
|
|
}
|
|
#[cfg(feature = "optimism")]
|
|
126 => {
|
|
let (tx, buf) = TxDeposit::from_compact(buf, buf.len());
|
|
(Transaction::Deposit(tx), buf)
|
|
}
|
|
_ => unreachable!("Junk data in database: unknown Transaction variant"),
|
|
}
|
|
}
|
|
_ => unreachable!("Junk data in database: unknown Transaction variant"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for Transaction {
|
|
fn default() -> Self {
|
|
Self::Legacy(TxLegacy::default())
|
|
}
|
|
}
|
|
|
|
/// This encodes the transaction _without_ the signature, and is only suitable for creating a hash
|
|
/// intended for signing.
|
|
impl Encodable for Transaction {
|
|
fn encode(&self, out: &mut dyn bytes::BufMut) {
|
|
match self {
|
|
Transaction::Legacy(legacy_tx) => {
|
|
legacy_tx.encode_for_signing(out);
|
|
}
|
|
Transaction::Eip2930(access_list_tx) => {
|
|
access_list_tx.encode_for_signing(out);
|
|
}
|
|
Transaction::Eip1559(dynamic_fee_tx) => {
|
|
dynamic_fee_tx.encode_for_signing(out);
|
|
}
|
|
Transaction::Eip4844(blob_tx) => {
|
|
blob_tx.encode_for_signing(out);
|
|
}
|
|
#[cfg(feature = "optimism")]
|
|
Transaction::Deposit(deposit_tx) => {
|
|
deposit_tx.encode(out, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn length(&self) -> usize {
|
|
match self {
|
|
Transaction::Legacy(legacy_tx) => legacy_tx.payload_len_for_signature(),
|
|
Transaction::Eip2930(access_list_tx) => access_list_tx.payload_len_for_signature(),
|
|
Transaction::Eip1559(dynamic_fee_tx) => dynamic_fee_tx.payload_len_for_signature(),
|
|
Transaction::Eip4844(blob_tx) => blob_tx.payload_len_for_signature(),
|
|
#[cfg(feature = "optimism")]
|
|
Transaction::Deposit(deposit_tx) => deposit_tx.payload_len(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Whether or not the transaction is a contract creation.
|
|
#[derive_arbitrary(compact, rlp)]
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
|
|
pub enum TransactionKind {
|
|
/// A transaction that creates a contract.
|
|
#[default]
|
|
Create,
|
|
/// A transaction that calls a contract or transfer.
|
|
Call(Address),
|
|
}
|
|
|
|
impl TransactionKind {
|
|
/// Returns the address of the contract that will be called or will receive the transfer.
|
|
pub fn to(self) -> Option<Address> {
|
|
match self {
|
|
TransactionKind::Create => None,
|
|
TransactionKind::Call(to) => Some(to),
|
|
}
|
|
}
|
|
|
|
/// Returns true if the transaction is a contract creation.
|
|
#[inline]
|
|
pub fn is_create(self) -> bool {
|
|
matches!(self, TransactionKind::Create)
|
|
}
|
|
|
|
/// Returns true if the transaction is a contract call.
|
|
#[inline]
|
|
pub fn is_call(self) -> bool {
|
|
matches!(self, TransactionKind::Call(_))
|
|
}
|
|
|
|
/// Calculates a heuristic for the in-memory size of the [TransactionKind].
|
|
#[inline]
|
|
fn size(self) -> usize {
|
|
mem::size_of::<Self>()
|
|
}
|
|
}
|
|
|
|
impl Compact for TransactionKind {
|
|
fn to_compact<B>(self, buf: &mut B) -> usize
|
|
where
|
|
B: bytes::BufMut + AsMut<[u8]>,
|
|
{
|
|
match self {
|
|
TransactionKind::Create => 0,
|
|
TransactionKind::Call(address) => {
|
|
address.to_compact(buf);
|
|
1
|
|
}
|
|
}
|
|
}
|
|
|
|
fn from_compact(buf: &[u8], identifier: usize) -> (Self, &[u8]) {
|
|
match identifier {
|
|
0 => (TransactionKind::Create, buf),
|
|
1 => {
|
|
let (addr, buf) = Address::from_compact(buf, buf.len());
|
|
(TransactionKind::Call(addr), buf)
|
|
}
|
|
_ => unreachable!("Junk data in database: unknown TransactionKind variant"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Encodable for TransactionKind {
|
|
fn encode(&self, out: &mut dyn alloy_rlp::BufMut) {
|
|
match self {
|
|
TransactionKind::Call(to) => to.encode(out),
|
|
TransactionKind::Create => out.put_u8(EMPTY_STRING_CODE),
|
|
}
|
|
}
|
|
fn length(&self) -> usize {
|
|
match self {
|
|
TransactionKind::Call(to) => to.length(),
|
|
TransactionKind::Create => 1, // EMPTY_STRING_CODE is a single byte
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Decodable for TransactionKind {
|
|
fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
|
|
if let Some(&first) = buf.first() {
|
|
if first == EMPTY_STRING_CODE {
|
|
buf.advance(1);
|
|
Ok(TransactionKind::Create)
|
|
} else {
|
|
let addr = <Address as Decodable>::decode(buf)?;
|
|
Ok(TransactionKind::Call(addr))
|
|
}
|
|
} else {
|
|
Err(RlpError::InputTooShort)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Signed transaction without its Hash. Used type for inserting into the DB.
|
|
///
|
|
/// This can by converted to [`TransactionSigned`] by calling [`TransactionSignedNoHash::hash`].
|
|
#[derive_arbitrary(compact)]
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, AsRef, Deref, Default, Serialize, Deserialize)]
|
|
pub struct TransactionSignedNoHash {
|
|
/// The transaction signature values
|
|
pub signature: Signature,
|
|
/// Raw transaction info
|
|
#[deref]
|
|
#[as_ref]
|
|
pub transaction: Transaction,
|
|
}
|
|
|
|
impl TransactionSignedNoHash {
|
|
/// Calculates the transaction hash. If used more than once, it's better to convert it to
|
|
/// [`TransactionSigned`] first.
|
|
pub fn hash(&self) -> B256 {
|
|
// pre-allocate buffer for the transaction
|
|
let mut buf = Vec::with_capacity(128 + self.transaction.input().len());
|
|
self.transaction.encode_with_signature(&self.signature, &mut buf, false);
|
|
keccak256(&buf)
|
|
}
|
|
|
|
/// Recover signer from signature and hash.
|
|
///
|
|
/// Returns `None` if the transaction's signature is invalid, see also [Self::recover_signer].
|
|
pub fn recover_signer(&self) -> Option<Address> {
|
|
let signature_hash = self.signature_hash();
|
|
self.signature.recover_signer(signature_hash)
|
|
}
|
|
|
|
/// Converts into a transaction type with its hash: [`TransactionSigned`].
|
|
pub fn with_hash(self) -> TransactionSigned {
|
|
self.into()
|
|
}
|
|
|
|
/// Recovers a list of signers from a transaction list iterator
|
|
///
|
|
/// Returns `None`, if some transaction's signature is invalid, see also
|
|
/// [Self::recover_signer].
|
|
pub fn recover_signers<'a, T>(txes: T, num_txes: usize) -> Option<Vec<Address>>
|
|
where
|
|
T: IntoParallelIterator<Item = &'a Self> + IntoIterator<Item = &'a Self> + Send,
|
|
{
|
|
if num_txes < *PARALLEL_SENDER_RECOVERY_THRESHOLD {
|
|
txes.into_iter().map(|tx| tx.recover_signer()).collect()
|
|
} else {
|
|
txes.into_par_iter().map(|tx| tx.recover_signer()).collect()
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Compact for TransactionSignedNoHash {
|
|
fn to_compact<B>(self, buf: &mut B) -> usize
|
|
where
|
|
B: bytes::BufMut + AsMut<[u8]>,
|
|
{
|
|
let start = buf.as_mut().len();
|
|
|
|
// Placeholder for bitflags.
|
|
// The first byte uses 4 bits as flags: IsCompressed[1bit], TxType[2bits], Signature[1bit]
|
|
buf.put_u8(0);
|
|
|
|
let sig_bit = self.signature.to_compact(buf) as u8;
|
|
let zstd_bit = self.transaction.input().len() >= 32;
|
|
|
|
let tx_bits = if zstd_bit {
|
|
TRANSACTION_COMPRESSOR.with(|compressor| {
|
|
let mut compressor = compressor.borrow_mut();
|
|
let mut tmp = bytes::BytesMut::with_capacity(200);
|
|
let tx_bits = self.transaction.to_compact(&mut tmp);
|
|
|
|
buf.put_slice(&compressor.compress(&tmp).expect("Failed to compress"));
|
|
tx_bits as u8
|
|
})
|
|
} else {
|
|
self.transaction.to_compact(buf) as u8
|
|
};
|
|
|
|
// Replace bitflags with the actual values
|
|
buf.as_mut()[start] = sig_bit | (tx_bits << 1) | ((zstd_bit as u8) << 3);
|
|
|
|
buf.as_mut().len() - start
|
|
}
|
|
|
|
fn from_compact(mut buf: &[u8], _len: usize) -> (Self, &[u8]) {
|
|
// The first byte uses 4 bits as flags: IsCompressed[1], TxType[2], Signature[1]
|
|
let bitflags = buf.get_u8() as usize;
|
|
|
|
let sig_bit = bitflags & 1;
|
|
let (signature, buf) = Signature::from_compact(buf, sig_bit);
|
|
|
|
let zstd_bit = bitflags >> 3;
|
|
let (transaction, buf) = if zstd_bit != 0 {
|
|
TRANSACTION_DECOMPRESSOR.with(|decompressor| {
|
|
let mut decompressor = decompressor.borrow_mut();
|
|
let mut tmp: Vec<u8> = Vec::with_capacity(200);
|
|
|
|
// `decompress_to_buffer` will return an error if the output buffer doesn't have
|
|
// enough capacity. However we don't actually have information on the required
|
|
// length. So we hope for the best, and keep trying again with a fairly bigger size
|
|
// if it fails.
|
|
while let Err(err) = decompressor.decompress_to_buffer(buf, &mut tmp) {
|
|
let err = err.to_string();
|
|
if !err.contains("Destination buffer is too small") {
|
|
panic!("Failed to decompress: {}", err);
|
|
}
|
|
tmp.reserve(tmp.capacity() + 24_000);
|
|
}
|
|
|
|
// TODO: enforce that zstd is only present at a "top" level type
|
|
|
|
let transaction_type = (bitflags & 0b110) >> 1;
|
|
let (transaction, _) = Transaction::from_compact(tmp.as_slice(), transaction_type);
|
|
|
|
(transaction, buf)
|
|
})
|
|
} else {
|
|
let transaction_type = bitflags >> 1;
|
|
Transaction::from_compact(buf, transaction_type)
|
|
};
|
|
|
|
(TransactionSignedNoHash { signature, transaction }, buf)
|
|
}
|
|
}
|
|
|
|
impl From<TransactionSignedNoHash> for TransactionSigned {
|
|
fn from(tx: TransactionSignedNoHash) -> Self {
|
|
TransactionSigned::from_transaction_and_signature(tx.transaction, tx.signature)
|
|
}
|
|
}
|
|
|
|
impl From<TransactionSigned> for TransactionSignedNoHash {
|
|
fn from(tx: TransactionSigned) -> Self {
|
|
TransactionSignedNoHash { signature: tx.signature, transaction: tx.transaction }
|
|
}
|
|
}
|
|
|
|
/// Signed transaction.
|
|
#[add_arbitrary_tests(rlp)]
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, AsRef, Deref, Default, Serialize, Deserialize)]
|
|
pub struct TransactionSigned {
|
|
/// Transaction hash
|
|
pub hash: TxHash,
|
|
/// The transaction signature values
|
|
pub signature: Signature,
|
|
/// Raw transaction info
|
|
#[deref]
|
|
#[as_ref]
|
|
pub transaction: Transaction,
|
|
}
|
|
|
|
impl AsRef<Self> for TransactionSigned {
|
|
fn as_ref(&self) -> &Self {
|
|
self
|
|
}
|
|
}
|
|
|
|
// === impl TransactionSigned ===
|
|
|
|
impl TransactionSigned {
|
|
/// Transaction signature.
|
|
pub fn signature(&self) -> &Signature {
|
|
&self.signature
|
|
}
|
|
|
|
/// Transaction hash. Used to identify transaction.
|
|
pub fn hash(&self) -> TxHash {
|
|
self.hash
|
|
}
|
|
|
|
/// Reference to transaction hash. Used to identify transaction.
|
|
pub fn hash_ref(&self) -> &TxHash {
|
|
&self.hash
|
|
}
|
|
|
|
/// Recover signer from signature and hash.
|
|
///
|
|
/// Returns `None` if the transaction's signature is invalid, see also [Self::recover_signer].
|
|
pub fn recover_signer(&self) -> Option<Address> {
|
|
// Optimism's Deposit transaction does not have a signature. Directly return the
|
|
// `from` address.
|
|
#[cfg(feature = "optimism")]
|
|
if let Transaction::Deposit(TxDeposit { from, .. }) = self.transaction {
|
|
return Some(from)
|
|
}
|
|
let signature_hash = self.signature_hash();
|
|
self.signature.recover_signer(signature_hash)
|
|
}
|
|
|
|
/// Recover signer from signature and hash _without ensuring that the signature has a low `s`
|
|
/// value_.
|
|
///
|
|
/// Returns `None` if the transaction's signature is invalid, see also
|
|
/// [Self::recover_signer_unchecked].
|
|
pub fn recover_signer_unchecked(&self) -> Option<Address> {
|
|
// Optimism's Deposit transaction does not have a signature. Directly return the
|
|
// `from` address.
|
|
#[cfg(feature = "optimism")]
|
|
if let Transaction::Deposit(TxDeposit { from, .. }) = self.transaction {
|
|
return Some(from)
|
|
}
|
|
let signature_hash = self.signature_hash();
|
|
self.signature.recover_signer_unchecked(signature_hash)
|
|
}
|
|
|
|
/// Recovers a list of signers from a transaction list iterator
|
|
///
|
|
/// Returns `None`, if some transaction's signature is invalid, see also
|
|
/// [Self::recover_signer].
|
|
pub fn recover_signers<'a, T>(txes: T, num_txes: usize) -> Option<Vec<Address>>
|
|
where
|
|
T: IntoParallelIterator<Item = &'a Self> + IntoIterator<Item = &'a Self> + Send,
|
|
{
|
|
if num_txes < *PARALLEL_SENDER_RECOVERY_THRESHOLD {
|
|
txes.into_iter().map(|tx| tx.recover_signer()).collect()
|
|
} else {
|
|
txes.into_par_iter().map(|tx| tx.recover_signer()).collect()
|
|
}
|
|
}
|
|
|
|
/// Returns the [TransactionSignedEcRecovered] transaction with the given sender.
|
|
#[inline]
|
|
pub const fn with_signer(self, signer: Address) -> TransactionSignedEcRecovered {
|
|
TransactionSignedEcRecovered::from_signed_transaction(self, signer)
|
|
}
|
|
|
|
/// Consumes the type, recover signer and return [`TransactionSignedEcRecovered`]
|
|
///
|
|
/// Returns `None` if the transaction's signature is invalid, see also [Self::recover_signer].
|
|
pub fn into_ecrecovered(self) -> Option<TransactionSignedEcRecovered> {
|
|
let signer = self.recover_signer()?;
|
|
Some(TransactionSignedEcRecovered { signed_transaction: self, signer })
|
|
}
|
|
|
|
/// Tries to recover signer and return [`TransactionSignedEcRecovered`] by cloning the type.
|
|
pub fn try_ecrecovered(&self) -> Option<TransactionSignedEcRecovered> {
|
|
let signer = self.recover_signer()?;
|
|
Some(TransactionSignedEcRecovered { signed_transaction: self.clone(), signer })
|
|
}
|
|
|
|
/// Tries to recover signer and return [`TransactionSignedEcRecovered`].
|
|
///
|
|
/// Returns `Err(Self)` if the transaction's signature is invalid, see also
|
|
/// [Self::recover_signer].
|
|
pub fn try_into_ecrecovered(self) -> Result<TransactionSignedEcRecovered, Self> {
|
|
match self.recover_signer() {
|
|
None => Err(self),
|
|
Some(signer) => Ok(TransactionSignedEcRecovered { signed_transaction: self, signer }),
|
|
}
|
|
}
|
|
|
|
/// Returns the enveloped encoded transactions.
|
|
///
|
|
/// See also [TransactionSigned::encode_enveloped]
|
|
pub fn envelope_encoded(&self) -> Bytes {
|
|
let mut buf = BytesMut::new();
|
|
self.encode_enveloped(&mut buf);
|
|
buf.freeze().into()
|
|
}
|
|
|
|
/// Encodes the transaction into the "raw" format (e.g. `eth_sendRawTransaction`).
|
|
/// This format is also referred to as "binary" encoding.
|
|
///
|
|
/// For legacy transactions, it encodes the RLP of the transaction into the buffer: `rlp(tx)`
|
|
/// For EIP-2718 typed it encodes the type of the transaction followed by the rlp of the
|
|
/// transaction: `type || rlp(tx)`
|
|
pub fn encode_enveloped(&self, out: &mut dyn bytes::BufMut) {
|
|
self.encode_inner(out, false)
|
|
}
|
|
|
|
/// Inner encoding function that is used for both rlp [`Encodable`] trait and for calculating
|
|
/// hash that for eip2718 does not require rlp header
|
|
pub(crate) fn encode_inner(&self, out: &mut dyn bytes::BufMut, with_header: bool) {
|
|
self.transaction.encode_with_signature(&self.signature, out, with_header);
|
|
}
|
|
|
|
/// Output the length of the encode_inner(out, true). Note to assume that `with_header` is only
|
|
/// `true`.
|
|
pub(crate) fn payload_len_inner(&self) -> usize {
|
|
match &self.transaction {
|
|
Transaction::Legacy(legacy_tx) => legacy_tx.payload_len_with_signature(&self.signature),
|
|
Transaction::Eip2930(access_list_tx) => {
|
|
access_list_tx.payload_len_with_signature(&self.signature)
|
|
}
|
|
Transaction::Eip1559(dynamic_fee_tx) => {
|
|
dynamic_fee_tx.payload_len_with_signature(&self.signature)
|
|
}
|
|
Transaction::Eip4844(blob_tx) => blob_tx.payload_len_with_signature(&self.signature),
|
|
#[cfg(feature = "optimism")]
|
|
Transaction::Deposit(deposit_tx) => deposit_tx.payload_len(),
|
|
}
|
|
}
|
|
|
|
/// Calculate transaction hash, eip2728 transaction does not contain rlp header and start with
|
|
/// tx type.
|
|
pub fn recalculate_hash(&self) -> B256 {
|
|
let mut buf = Vec::new();
|
|
self.encode_inner(&mut buf, false);
|
|
keccak256(&buf)
|
|
}
|
|
|
|
/// Create a new signed transaction from a transaction and its signature.
|
|
/// This will also calculate the transaction hash using its encoding.
|
|
pub fn from_transaction_and_signature(transaction: Transaction, signature: Signature) -> Self {
|
|
let mut initial_tx = Self { transaction, hash: Default::default(), signature };
|
|
initial_tx.hash = initial_tx.recalculate_hash();
|
|
initial_tx
|
|
}
|
|
|
|
/// Calculate a heuristic for the in-memory size of the [TransactionSigned].
|
|
#[inline]
|
|
pub fn size(&self) -> usize {
|
|
mem::size_of::<TxHash>() + self.transaction.size() + self.signature.size()
|
|
}
|
|
|
|
/// Decodes legacy transaction from the data buffer into a tuple.
|
|
///
|
|
/// This expects `rlp(legacy_tx)`
|
|
///
|
|
/// Refer to the docs for [Self::decode_rlp_legacy_transaction] for details on the exact
|
|
/// format expected.
|
|
pub(crate) fn decode_rlp_legacy_transaction_tuple(
|
|
data: &mut &[u8],
|
|
) -> alloy_rlp::Result<(TxLegacy, TxHash, Signature)> {
|
|
// keep this around, so we can use it to calculate the hash
|
|
let original_encoding = *data;
|
|
|
|
let header = Header::decode(data)?;
|
|
let remaining_len = data.len();
|
|
|
|
let transaction_payload_len = header.payload_length;
|
|
|
|
if transaction_payload_len > remaining_len {
|
|
return Err(RlpError::InputTooShort)
|
|
}
|
|
|
|
let mut transaction = TxLegacy {
|
|
nonce: Decodable::decode(data)?,
|
|
gas_price: Decodable::decode(data)?,
|
|
gas_limit: Decodable::decode(data)?,
|
|
to: Decodable::decode(data)?,
|
|
value: Decodable::decode(data)?,
|
|
input: Decodable::decode(data)?,
|
|
chain_id: None,
|
|
};
|
|
let (signature, extracted_id) = Signature::decode_with_eip155_chain_id(data)?;
|
|
transaction.chain_id = extracted_id;
|
|
|
|
// check the new length, compared to the original length and the header length
|
|
let decoded = remaining_len - data.len();
|
|
if decoded != transaction_payload_len {
|
|
return Err(RlpError::UnexpectedLength)
|
|
}
|
|
|
|
let tx_length = header.payload_length + header.length();
|
|
let hash = keccak256(&original_encoding[..tx_length]);
|
|
Ok((transaction, hash, signature))
|
|
}
|
|
|
|
/// Decodes legacy transaction from the data buffer.
|
|
///
|
|
/// This should be used _only_ be used in general transaction decoding methods, which have
|
|
/// already ensured that the input is a legacy transaction with the following format:
|
|
/// `rlp(legacy_tx)`
|
|
///
|
|
/// Legacy transactions are encoded as lists, so the input should start with a RLP list header.
|
|
///
|
|
/// This expects `rlp(legacy_tx)`
|
|
// TODO: make buf advancement semantics consistent with `decode_enveloped_typed_transaction`,
|
|
// so decoding methods do not need to manually advance the buffer
|
|
pub fn decode_rlp_legacy_transaction(data: &mut &[u8]) -> alloy_rlp::Result<TransactionSigned> {
|
|
let (transaction, hash, signature) =
|
|
TransactionSigned::decode_rlp_legacy_transaction_tuple(data)?;
|
|
let signed =
|
|
TransactionSigned { transaction: Transaction::Legacy(transaction), hash, signature };
|
|
Ok(signed)
|
|
}
|
|
|
|
/// Decodes en enveloped EIP-2718 typed transaction.
|
|
///
|
|
/// This should be used _only_ be used internally in general transaction decoding methods,
|
|
/// which have already ensured that the input is a typed transaction with the following format:
|
|
/// `tx_type || rlp(tx)`
|
|
///
|
|
/// Note that this format does not start with any RLP header, and instead starts with a single
|
|
/// byte indicating the transaction type.
|
|
///
|
|
/// CAUTION: this expects that `data` is `tx_type || rlp(tx)`
|
|
pub fn decode_enveloped_typed_transaction(
|
|
data: &mut &[u8],
|
|
) -> alloy_rlp::Result<TransactionSigned> {
|
|
// keep this around so we can use it to calculate the hash
|
|
let original_encoding = *data;
|
|
|
|
let tx_type = *data.first().ok_or(RlpError::InputTooShort)?;
|
|
data.advance(1);
|
|
|
|
// decode the list header for the rest of the transaction
|
|
let header = Header::decode(data)?;
|
|
if !header.list {
|
|
return Err(RlpError::Custom("typed tx fields must be encoded as a list"))
|
|
}
|
|
|
|
let remaining_len = data.len();
|
|
|
|
// length of tx encoding = tx type byte (size = 1) + length of header + payload length
|
|
let tx_length = 1 + header.length() + header.payload_length;
|
|
|
|
// decode common fields
|
|
let transaction = match tx_type {
|
|
1 => Transaction::Eip2930(TxEip2930::decode_inner(data)?),
|
|
2 => Transaction::Eip1559(TxEip1559::decode_inner(data)?),
|
|
3 => Transaction::Eip4844(TxEip4844::decode_inner(data)?),
|
|
#[cfg(feature = "optimism")]
|
|
0x7E => Transaction::Deposit(TxDeposit::decode_inner(data)?),
|
|
_ => return Err(RlpError::Custom("unsupported typed transaction type")),
|
|
};
|
|
|
|
#[cfg(not(feature = "optimism"))]
|
|
let signature = Signature::decode(data)?;
|
|
|
|
#[cfg(feature = "optimism")]
|
|
let signature = if tx_type == DEPOSIT_TX_TYPE_ID {
|
|
Signature::optimism_deposit_tx_signature()
|
|
} else {
|
|
Signature::decode(data)?
|
|
};
|
|
|
|
let bytes_consumed = remaining_len - data.len();
|
|
if bytes_consumed != header.payload_length {
|
|
return Err(RlpError::UnexpectedLength)
|
|
}
|
|
|
|
let hash = keccak256(&original_encoding[..tx_length]);
|
|
let signed = TransactionSigned { transaction, hash, signature };
|
|
Ok(signed)
|
|
}
|
|
|
|
/// Decodes the "raw" format of transaction (similar to `eth_sendRawTransaction`).
|
|
///
|
|
/// This should be used for any RPC method that accepts a raw transaction, **excluding** raw
|
|
/// EIP-4844 transactions in `eth_sendRawTransaction`. Currently, this includes:
|
|
/// * `eth_sendRawTransaction` for non-EIP-4844 transactions.
|
|
/// * All versions of `engine_newPayload`, in the `transactions` field.
|
|
///
|
|
/// A raw transaction is either a legacy transaction or EIP-2718 typed transaction.
|
|
///
|
|
/// For legacy transactions, the format is encoded as: `rlp(tx)`. This format will start with a
|
|
/// RLP list header.
|
|
///
|
|
/// For EIP-2718 typed transactions, the format is encoded as the type of the transaction
|
|
/// followed by the rlp of the transaction: `type || rlp(tx)`.
|
|
///
|
|
/// To decode EIP-4844 transactions in `eth_sendRawTransaction`, use
|
|
/// [PooledTransactionsElement::decode_enveloped].
|
|
pub fn decode_enveloped(data: &mut &[u8]) -> alloy_rlp::Result<Self> {
|
|
if data.is_empty() {
|
|
return Err(RlpError::InputTooShort)
|
|
}
|
|
|
|
// Check if the tx is a list
|
|
if data[0] >= EMPTY_LIST_CODE {
|
|
// decode as legacy transaction
|
|
TransactionSigned::decode_rlp_legacy_transaction(data)
|
|
} else {
|
|
TransactionSigned::decode_enveloped_typed_transaction(data)
|
|
}
|
|
}
|
|
|
|
/// Returns the length without an RLP header - this is used for eth/68 sizes.
|
|
pub fn length_without_header(&self) -> usize {
|
|
// method computes the payload len without a RLP header
|
|
match &self.transaction {
|
|
Transaction::Legacy(legacy_tx) => legacy_tx.payload_len_with_signature(&self.signature),
|
|
Transaction::Eip2930(access_list_tx) => {
|
|
access_list_tx.payload_len_with_signature_without_header(&self.signature)
|
|
}
|
|
Transaction::Eip1559(dynamic_fee_tx) => {
|
|
dynamic_fee_tx.payload_len_with_signature_without_header(&self.signature)
|
|
}
|
|
Transaction::Eip4844(blob_tx) => {
|
|
blob_tx.payload_len_with_signature_without_header(&self.signature)
|
|
}
|
|
#[cfg(feature = "optimism")]
|
|
Transaction::Deposit(deposit_tx) => deposit_tx.payload_len_without_header(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<TransactionSignedEcRecovered> for TransactionSigned {
|
|
fn from(recovered: TransactionSignedEcRecovered) -> Self {
|
|
recovered.signed_transaction
|
|
}
|
|
}
|
|
|
|
impl Encodable for TransactionSigned {
|
|
fn encode(&self, out: &mut dyn bytes::BufMut) {
|
|
self.encode_inner(out, true);
|
|
}
|
|
|
|
fn length(&self) -> usize {
|
|
self.payload_len_inner()
|
|
}
|
|
}
|
|
|
|
/// This `Decodable` implementation only supports decoding rlp encoded transactions as it's used by
|
|
/// p2p.
|
|
///
|
|
/// The p2p encoding format always includes an RLP header, although the type RLP header depends on
|
|
/// whether or not the transaction is a legacy transaction.
|
|
///
|
|
/// If the transaction is a legacy transaction, it is just encoded as a RLP list: `rlp(tx)`.
|
|
///
|
|
/// If the transaction is a typed transaction, it is encoded as a RLP string:
|
|
/// `rlp(type || rlp(tx))`
|
|
///
|
|
/// This cannot be used for decoding EIP-4844 transactions in p2p, since the EIP-4844 variant of
|
|
/// [TransactionSigned] does not include the blob sidecar. For a general purpose decoding method
|
|
/// suitable for decoding transactions from p2p, see [PooledTransactionsElement].
|
|
///
|
|
/// CAUTION: Due to a quirk in [Header::decode], this method will succeed even if a typed
|
|
/// transaction is encoded in the RPC format, and does not start with a RLP header. This is because
|
|
/// [Header::decode] does not advance the buffer, and returns a length-1 string header if the first
|
|
/// byte is less than `0xf7`. This causes this decode implementation to pass unaltered buffer to
|
|
/// [TransactionSigned::decode_enveloped_typed_transaction], which expects the RPC format. Despite
|
|
/// this quirk, this should **not** be used for RPC methods that accept raw transactions.
|
|
impl Decodable for TransactionSigned {
|
|
fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
|
|
// decode header
|
|
let mut original_encoding = *buf;
|
|
let header = Header::decode(buf)?;
|
|
|
|
let remaining_len = buf.len();
|
|
|
|
// if the transaction is encoded as a string then it is a typed transaction
|
|
if !header.list {
|
|
let tx = TransactionSigned::decode_enveloped_typed_transaction(buf)?;
|
|
|
|
let bytes_consumed = remaining_len - buf.len();
|
|
// because Header::decode works for single bytes (including the tx type), returning a
|
|
// string Header with payload_length of 1, we need to make sure this check is only
|
|
// performed for transactions with a string header
|
|
if bytes_consumed != header.payload_length && original_encoding[0] > EMPTY_STRING_CODE {
|
|
return Err(RlpError::UnexpectedLength)
|
|
}
|
|
|
|
Ok(tx)
|
|
} else {
|
|
let tx = TransactionSigned::decode_rlp_legacy_transaction(&mut original_encoding)?;
|
|
|
|
// advance the buffer based on how far `decode_rlp_legacy_transaction` advanced the
|
|
// buffer
|
|
*buf = original_encoding;
|
|
Ok(tx)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(any(test, feature = "arbitrary"))]
|
|
impl proptest::arbitrary::Arbitrary for TransactionSigned {
|
|
type Parameters = ();
|
|
fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
|
|
use proptest::prelude::{any, Strategy};
|
|
|
|
any::<(Transaction, Signature)>()
|
|
.prop_map(move |(mut transaction, sig)| {
|
|
if let Some(chain_id) = transaction.chain_id() {
|
|
// Otherwise we might overflow when calculating `v` on `recalculate_hash`
|
|
transaction.set_chain_id(chain_id % (u64::MAX / 2 - 36));
|
|
}
|
|
|
|
#[cfg(feature = "optimism")]
|
|
let sig = transaction
|
|
.is_deposit()
|
|
.then(Signature::optimism_deposit_tx_signature)
|
|
.unwrap_or(sig);
|
|
|
|
let mut tx =
|
|
TransactionSigned { hash: Default::default(), signature: sig, transaction };
|
|
tx.hash = tx.recalculate_hash();
|
|
tx
|
|
})
|
|
.boxed()
|
|
}
|
|
|
|
type Strategy = proptest::strategy::BoxedStrategy<TransactionSigned>;
|
|
}
|
|
|
|
#[cfg(any(test, feature = "arbitrary"))]
|
|
impl<'a> arbitrary::Arbitrary<'a> for TransactionSigned {
|
|
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
|
|
let mut transaction = Transaction::arbitrary(u)?;
|
|
if let Some(chain_id) = transaction.chain_id() {
|
|
// Otherwise we might overflow when calculating `v` on `recalculate_hash`
|
|
transaction.set_chain_id(chain_id % (u64::MAX / 2 - 36));
|
|
}
|
|
|
|
let signature = Signature::arbitrary(u)?;
|
|
|
|
#[cfg(feature = "optimism")]
|
|
let signature = if transaction.is_deposit() {
|
|
Signature { r: crate::U256::ZERO, s: crate::U256::ZERO, odd_y_parity: false }
|
|
} else {
|
|
signature
|
|
};
|
|
|
|
Ok(TransactionSigned::from_transaction_and_signature(transaction, signature))
|
|
}
|
|
}
|
|
|
|
/// Signed transaction with recovered signer.
|
|
#[derive(Debug, Clone, PartialEq, Hash, Eq, AsRef, Deref, Default)]
|
|
pub struct TransactionSignedEcRecovered {
|
|
/// Signer of the transaction
|
|
signer: Address,
|
|
/// Signed transaction
|
|
#[deref]
|
|
#[as_ref]
|
|
signed_transaction: TransactionSigned,
|
|
}
|
|
|
|
// === impl TransactionSignedEcRecovered ===
|
|
|
|
impl TransactionSignedEcRecovered {
|
|
/// Signer of transaction recovered from signature
|
|
pub fn signer(&self) -> Address {
|
|
self.signer
|
|
}
|
|
|
|
/// Transform back to [`TransactionSigned`]
|
|
pub fn into_signed(self) -> TransactionSigned {
|
|
self.signed_transaction
|
|
}
|
|
|
|
/// Desolve Self to its component
|
|
pub fn to_components(self) -> (TransactionSigned, Address) {
|
|
(self.signed_transaction, self.signer)
|
|
}
|
|
|
|
/// Create [`TransactionSignedEcRecovered`] from [`TransactionSigned`] and [`Address`] of the
|
|
/// signer.
|
|
#[inline]
|
|
pub const fn from_signed_transaction(
|
|
signed_transaction: TransactionSigned,
|
|
signer: Address,
|
|
) -> Self {
|
|
Self { signed_transaction, signer }
|
|
}
|
|
}
|
|
|
|
impl Encodable for TransactionSignedEcRecovered {
|
|
fn encode(&self, out: &mut dyn bytes::BufMut) {
|
|
self.signed_transaction.encode(out)
|
|
}
|
|
|
|
fn length(&self) -> usize {
|
|
self.signed_transaction.length()
|
|
}
|
|
}
|
|
|
|
impl Decodable for TransactionSignedEcRecovered {
|
|
fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
|
|
let signed_transaction = TransactionSigned::decode(buf)?;
|
|
let signer = signed_transaction
|
|
.recover_signer()
|
|
.ok_or(RlpError::Custom("Unable to recover decoded transaction signer."))?;
|
|
Ok(TransactionSignedEcRecovered { signer, signed_transaction })
|
|
}
|
|
}
|
|
|
|
/// A transaction type that can be created from a [`TransactionSignedEcRecovered`] transaction.
|
|
///
|
|
/// This is a conversion trait that'll ensure transactions received via P2P can be converted to the
|
|
/// transaction type that the transaction pool uses.
|
|
pub trait FromRecoveredTransaction {
|
|
/// Converts to this type from the given [`TransactionSignedEcRecovered`].
|
|
fn from_recovered_transaction(tx: TransactionSignedEcRecovered) -> Self;
|
|
}
|
|
|
|
// Noop conversion
|
|
impl FromRecoveredTransaction for TransactionSignedEcRecovered {
|
|
#[inline]
|
|
fn from_recovered_transaction(tx: TransactionSignedEcRecovered) -> Self {
|
|
tx
|
|
}
|
|
}
|
|
|
|
/// A transaction type that can be created from a [`PooledTransactionsElementEcRecovered`]
|
|
/// transaction.
|
|
///
|
|
/// This is a conversion trait that'll ensure transactions received via P2P can be converted to the
|
|
/// transaction type that the transaction pool uses.
|
|
#[cfg(feature = "c-kzg")]
|
|
pub trait FromRecoveredPooledTransaction {
|
|
/// Converts to this type from the given [`PooledTransactionsElementEcRecovered`].
|
|
fn from_recovered_pooled_transaction(tx: PooledTransactionsElementEcRecovered) -> Self;
|
|
}
|
|
|
|
/// The inverse of [`FromRecoveredTransaction`] that ensure the transaction can be sent over the
|
|
/// network
|
|
pub trait IntoRecoveredTransaction {
|
|
/// Converts to this type into a [`TransactionSignedEcRecovered`].
|
|
///
|
|
/// Note: this takes `&self` since indented usage is via `Arc<Self>`.
|
|
fn to_recovered_transaction(&self) -> TransactionSignedEcRecovered;
|
|
}
|
|
|
|
impl IntoRecoveredTransaction for TransactionSignedEcRecovered {
|
|
#[inline]
|
|
fn to_recovered_transaction(&self) -> TransactionSignedEcRecovered {
|
|
self.clone()
|
|
}
|
|
}
|
|
|
|
/// Either a transaction hash or number.
|
|
pub type TxHashOrNumber = BlockHashOrNumber;
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::{
|
|
hex, sign_message,
|
|
transaction::{
|
|
signature::Signature, TransactionKind, TxEip1559, TxLegacy,
|
|
PARALLEL_SENDER_RECOVERY_THRESHOLD,
|
|
},
|
|
Address, Bytes, Transaction, TransactionSigned, TransactionSignedEcRecovered, B256, U256,
|
|
};
|
|
use alloy_primitives::{b256, bytes};
|
|
use alloy_rlp::{Decodable, Encodable, Error as RlpError};
|
|
use bytes::BytesMut;
|
|
use secp256k1::{KeyPair, Secp256k1};
|
|
use std::str::FromStr;
|
|
|
|
#[test]
|
|
fn test_decode_empty_typed_tx() {
|
|
let input = [0x80u8];
|
|
let res = TransactionSigned::decode(&mut &input[..]).unwrap_err();
|
|
assert_eq!(RlpError::InputTooShort, res);
|
|
}
|
|
|
|
#[test]
|
|
fn test_decode_create_goerli() {
|
|
// test that an example create tx from goerli decodes properly
|
|
let tx_bytes = hex!("b901f202f901ee05228459682f008459682f11830209bf8080b90195608060405234801561001057600080fd5b50610175806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80630c49c36c14610030575b600080fd5b61003861004e565b604051610045919061011d565b60405180910390f35b60606020600052600f6020527f68656c6c6f2073746174656d696e64000000000000000000000000000000000060405260406000f35b600081519050919050565b600082825260208201905092915050565b60005b838110156100be5780820151818401526020810190506100a3565b838111156100cd576000848401525b50505050565b6000601f19601f8301169050919050565b60006100ef82610084565b6100f9818561008f565b93506101098185602086016100a0565b610112816100d3565b840191505092915050565b6000602082019050818103600083015261013781846100e4565b90509291505056fea264697066735822122051449585839a4ea5ac23cae4552ef8a96b64ff59d0668f76bfac3796b2bdbb3664736f6c63430008090033c080a0136ebffaa8fc8b9fda9124de9ccb0b1f64e90fbd44251b4c4ac2501e60b104f9a07eb2999eec6d185ef57e91ed099afb0a926c5b536f0155dd67e537c7476e1471");
|
|
|
|
let decoded = TransactionSigned::decode(&mut &tx_bytes[..]).unwrap();
|
|
assert_eq!(tx_bytes.len(), decoded.length());
|
|
|
|
let mut encoded = BytesMut::new();
|
|
decoded.encode(&mut encoded);
|
|
|
|
assert_eq!(tx_bytes, encoded[..]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_decode_recover_mainnet_tx() {
|
|
// random mainnet tx <https://etherscan.io/tx/0x86718885c4b4218c6af87d3d0b0d83e3cc465df2a05c048aa4db9f1a6f9de91f>
|
|
let tx_bytes = hex!("02f872018307910d808507204d2cb1827d0094388c818ca8b9251b393131c08a736a67ccb19297880320d04823e2701c80c001a0cf024f4815304df2867a1a74e9d2707b6abda0337d2d54a4438d453f4160f190a07ac0e6b3bc9395b5b9c8b9e6d77204a236577a5b18467b9175c01de4faa208d9");
|
|
|
|
let decoded = TransactionSigned::decode_enveloped(&mut &tx_bytes[..]).unwrap();
|
|
assert_eq!(
|
|
decoded.recover_signer(),
|
|
Some(Address::from_str("0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5").unwrap())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn decode_transaction_consumes_buffer() {
|
|
let bytes = &mut &hex!("b87502f872041a8459682f008459682f0d8252089461815774383099e24810ab832a5b2a5425c154d58829a2241af62c000080c001a059e6b67f48fb32e7e570dfb11e042b5ad2e55e3ce3ce9cd989c7e06e07feeafda0016b83f4f980694ed2eee4d10667242b1f40dc406901b34125b008d334d47469")[..];
|
|
let _transaction_res = TransactionSigned::decode(bytes).unwrap();
|
|
assert_eq!(
|
|
bytes.len(),
|
|
0,
|
|
"did not consume all bytes in the buffer, {:?} remaining",
|
|
bytes.len()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn decode_multiple_network_txs() {
|
|
let bytes = hex!("f86b02843b9aca00830186a094d3e8763675e4c425df46cc3b5c0f6cbdac39604687038d7ea4c68000802ba00eb96ca19e8a77102767a41fc85a36afd5c61ccb09911cec5d3e86e193d9c5aea03a456401896b1b6055311536bf00a718568c744d8c1f9df59879e8350220ca18");
|
|
let transaction = Transaction::Legacy(TxLegacy {
|
|
chain_id: Some(4u64),
|
|
nonce: 2,
|
|
gas_price: 1000000000,
|
|
gas_limit: 100000,
|
|
to: TransactionKind::Call(
|
|
Address::from_str("d3e8763675e4c425df46cc3b5c0f6cbdac396046").unwrap(),
|
|
),
|
|
value: 1000000000000000_u64.into(),
|
|
input: Bytes::default(),
|
|
});
|
|
let signature = Signature {
|
|
odd_y_parity: false,
|
|
r: U256::from_str("0xeb96ca19e8a77102767a41fc85a36afd5c61ccb09911cec5d3e86e193d9c5ae")
|
|
.unwrap(),
|
|
s: U256::from_str("0x3a456401896b1b6055311536bf00a718568c744d8c1f9df59879e8350220ca18")
|
|
.unwrap(),
|
|
};
|
|
let hash = b256!("a517b206d2223278f860ea017d3626cacad4f52ff51030dc9a96b432f17f8d34");
|
|
test_decode_and_encode(&bytes, transaction, signature, Some(hash));
|
|
|
|
let bytes = hex!("f86b01843b9aca00830186a094d3e8763675e4c425df46cc3b5c0f6cbdac3960468702769bb01b2a00802ba0e24d8bd32ad906d6f8b8d7741e08d1959df021698b19ee232feba15361587d0aa05406ad177223213df262cb66ccbb2f46bfdccfdfbbb5ffdda9e2c02d977631da");
|
|
let transaction = Transaction::Legacy(TxLegacy {
|
|
chain_id: Some(4),
|
|
nonce: 1u64,
|
|
gas_price: 1000000000,
|
|
gas_limit: 100000u64,
|
|
to: TransactionKind::Call(Address::from_slice(
|
|
&hex!("d3e8763675e4c425df46cc3b5c0f6cbdac396046")[..],
|
|
)),
|
|
value: 693361000000000_u64.into(),
|
|
input: Default::default(),
|
|
});
|
|
let signature = Signature {
|
|
odd_y_parity: false,
|
|
r: U256::from_str("0xe24d8bd32ad906d6f8b8d7741e08d1959df021698b19ee232feba15361587d0a")
|
|
.unwrap(),
|
|
s: U256::from_str("0x5406ad177223213df262cb66ccbb2f46bfdccfdfbbb5ffdda9e2c02d977631da")
|
|
.unwrap(),
|
|
};
|
|
test_decode_and_encode(&bytes, transaction, signature, None);
|
|
|
|
let bytes = hex!("f86b0384773594008398968094d3e8763675e4c425df46cc3b5c0f6cbdac39604687038d7ea4c68000802ba0ce6834447c0a4193c40382e6c57ae33b241379c5418caac9cdc18d786fd12071a03ca3ae86580e94550d7c071e3a02eadb5a77830947c9225165cf9100901bee88");
|
|
let transaction = Transaction::Legacy(TxLegacy {
|
|
chain_id: Some(4),
|
|
nonce: 3,
|
|
gas_price: 2000000000,
|
|
gas_limit: 10000000,
|
|
to: TransactionKind::Call(Address::from_slice(
|
|
&hex!("d3e8763675e4c425df46cc3b5c0f6cbdac396046")[..],
|
|
)),
|
|
value: 1000000000000000_u64.into(),
|
|
input: Bytes::default(),
|
|
});
|
|
let signature = Signature {
|
|
odd_y_parity: false,
|
|
r: U256::from_str("0xce6834447c0a4193c40382e6c57ae33b241379c5418caac9cdc18d786fd12071")
|
|
.unwrap(),
|
|
s: U256::from_str("0x3ca3ae86580e94550d7c071e3a02eadb5a77830947c9225165cf9100901bee88")
|
|
.unwrap(),
|
|
};
|
|
test_decode_and_encode(&bytes, transaction, signature, None);
|
|
|
|
let bytes = hex!("b87502f872041a8459682f008459682f0d8252089461815774383099e24810ab832a5b2a5425c154d58829a2241af62c000080c001a059e6b67f48fb32e7e570dfb11e042b5ad2e55e3ce3ce9cd989c7e06e07feeafda0016b83f4f980694ed2eee4d10667242b1f40dc406901b34125b008d334d47469");
|
|
let transaction = Transaction::Eip1559(TxEip1559 {
|
|
chain_id: 4,
|
|
nonce: 26,
|
|
max_priority_fee_per_gas: 1500000000,
|
|
max_fee_per_gas: 1500000013,
|
|
gas_limit: 21000,
|
|
to: TransactionKind::Call(Address::from_slice(
|
|
&hex!("61815774383099e24810ab832a5b2a5425c154d5")[..],
|
|
)),
|
|
value: 3000000000000000000_u64.into(),
|
|
input: Default::default(),
|
|
access_list: Default::default(),
|
|
});
|
|
let signature = Signature {
|
|
odd_y_parity: true,
|
|
r: U256::from_str("0x59e6b67f48fb32e7e570dfb11e042b5ad2e55e3ce3ce9cd989c7e06e07feeafd")
|
|
.unwrap(),
|
|
s: U256::from_str("0x016b83f4f980694ed2eee4d10667242b1f40dc406901b34125b008d334d47469")
|
|
.unwrap(),
|
|
};
|
|
test_decode_and_encode(&bytes, transaction, signature, None);
|
|
|
|
let bytes = hex!("f8650f84832156008287fb94cf7f9e66af820a19257a2108375b180b0ec491678204d2802ca035b7bfeb9ad9ece2cbafaaf8e202e706b4cfaeb233f46198f00b44d4a566a981a0612638fb29427ca33b9a3be2a0a561beecfe0269655be160d35e72d366a6a860");
|
|
let transaction = Transaction::Legacy(TxLegacy {
|
|
chain_id: Some(4),
|
|
nonce: 15,
|
|
gas_price: 2200000000,
|
|
gas_limit: 34811,
|
|
to: TransactionKind::Call(Address::from_slice(
|
|
&hex!("cf7f9e66af820a19257a2108375b180b0ec49167")[..],
|
|
)),
|
|
value: 1234_u64.into(),
|
|
input: Bytes::default(),
|
|
});
|
|
let signature = Signature {
|
|
odd_y_parity: true,
|
|
r: U256::from_str("0x35b7bfeb9ad9ece2cbafaaf8e202e706b4cfaeb233f46198f00b44d4a566a981")
|
|
.unwrap(),
|
|
s: U256::from_str("0x612638fb29427ca33b9a3be2a0a561beecfe0269655be160d35e72d366a6a860")
|
|
.unwrap(),
|
|
};
|
|
test_decode_and_encode(&bytes, transaction, signature, None);
|
|
}
|
|
|
|
fn test_decode_and_encode(
|
|
bytes: &[u8],
|
|
transaction: Transaction,
|
|
signature: Signature,
|
|
hash: Option<B256>,
|
|
) {
|
|
let expected = TransactionSigned::from_transaction_and_signature(transaction, signature);
|
|
if let Some(hash) = hash {
|
|
assert_eq!(hash, expected.hash);
|
|
}
|
|
assert_eq!(bytes.len(), expected.length());
|
|
|
|
let decoded = TransactionSigned::decode(&mut &bytes[..]).unwrap();
|
|
assert_eq!(expected, decoded);
|
|
|
|
let mut encoded = BytesMut::new();
|
|
expected.encode(&mut encoded);
|
|
assert_eq!(bytes, encoded);
|
|
}
|
|
|
|
#[test]
|
|
fn decode_raw_tx_and_recover_signer() {
|
|
use crate::hex_literal::hex;
|
|
// transaction is from ropsten
|
|
|
|
let hash: B256 =
|
|
hex!("559fb34c4a7f115db26cbf8505389475caaab3df45f5c7a0faa4abfa3835306c").into();
|
|
let signer: Address = hex!("641c5d790f862a58ec7abcfd644c0442e9c201b3").into();
|
|
let raw =hex!("f88b8212b085028fa6ae00830f424094aad593da0c8116ef7d2d594dd6a63241bccfc26c80a48318b64b000000000000000000000000641c5d790f862a58ec7abcfd644c0442e9c201b32aa0a6ef9e170bca5ffb7ac05433b13b7043de667fbb0b4a5e45d3b54fb2d6efcc63a0037ec2c05c3d60c5f5f78244ce0a3859e3a18a36c61efb061b383507d3ce19d2");
|
|
|
|
let mut pointer = raw.as_ref();
|
|
let tx = TransactionSigned::decode(&mut pointer).unwrap();
|
|
assert_eq!(tx.hash(), hash, "Expected same hash");
|
|
assert_eq!(tx.recover_signer(), Some(signer), "Recovering signer should pass.");
|
|
}
|
|
|
|
#[test]
|
|
fn test_envelop_encode() {
|
|
// random tx: <https://etherscan.io/getRawTx?tx=0x9448608d36e721ef403c53b00546068a6474d6cbab6816c3926de449898e7bce>
|
|
let input = hex!("02f871018302a90f808504890aef60826b6c94ddf4c5025d1a5742cf12f74eec246d4432c295e487e09c3bbcc12b2b80c080a0f21a4eacd0bf8fea9c5105c543be5a1d8c796516875710fafafdf16d16d8ee23a001280915021bb446d1973501a67f93d2b38894a514b976e7b46dc2fe54598d76");
|
|
let decoded = TransactionSigned::decode(&mut &input[..]).unwrap();
|
|
|
|
let encoded = decoded.envelope_encoded();
|
|
assert_eq!(encoded[..], input);
|
|
}
|
|
|
|
#[test]
|
|
fn test_envelop_decode() {
|
|
// random tx: <https://etherscan.io/getRawTx?tx=0x9448608d36e721ef403c53b00546068a6474d6cbab6816c3926de449898e7bce>
|
|
let input = bytes!("02f871018302a90f808504890aef60826b6c94ddf4c5025d1a5742cf12f74eec246d4432c295e487e09c3bbcc12b2b80c080a0f21a4eacd0bf8fea9c5105c543be5a1d8c796516875710fafafdf16d16d8ee23a001280915021bb446d1973501a67f93d2b38894a514b976e7b46dc2fe54598d76");
|
|
let decoded = TransactionSigned::decode_enveloped(&mut input.as_ref()).unwrap();
|
|
|
|
let encoded = decoded.envelope_encoded();
|
|
assert_eq!(encoded, input);
|
|
}
|
|
|
|
#[test]
|
|
fn test_decode_signed_ec_recovered_transaction() {
|
|
// random tx: <https://etherscan.io/getRawTx?tx=0x9448608d36e721ef403c53b00546068a6474d6cbab6816c3926de449898e7bce>
|
|
let input = hex!("02f871018302a90f808504890aef60826b6c94ddf4c5025d1a5742cf12f74eec246d4432c295e487e09c3bbcc12b2b80c080a0f21a4eacd0bf8fea9c5105c543be5a1d8c796516875710fafafdf16d16d8ee23a001280915021bb446d1973501a67f93d2b38894a514b976e7b46dc2fe54598d76");
|
|
let tx = TransactionSigned::decode(&mut &input[..]).unwrap();
|
|
let recovered = tx.into_ecrecovered().unwrap();
|
|
|
|
let mut encoded = BytesMut::new();
|
|
recovered.encode(&mut encoded);
|
|
|
|
let decoded = TransactionSignedEcRecovered::decode(&mut &encoded[..]).unwrap();
|
|
assert_eq!(recovered, decoded)
|
|
}
|
|
|
|
#[test]
|
|
fn test_decode_tx() {
|
|
// some random transactions pulled from hive tests
|
|
let s = "b86f02f86c0705843b9aca008506fc23ac00830124f89400000000000000000000000000000000000003160180c001a00293c713e2f1eab91c366621ff2f867e05ad7e99d4aa5d069aafeb9e1e8c9b6aa05ec6c0605ff20b57c90a6484ec3b0509e5923733d06f9b69bee9a2dabe4f1352";
|
|
let tx = TransactionSigned::decode(&mut &hex::decode(s).unwrap()[..]).unwrap();
|
|
let mut b = Vec::new();
|
|
tx.encode(&mut b);
|
|
assert_eq!(s, hex::encode(&b));
|
|
|
|
let s = "f865048506fc23ac00830124f8940000000000000000000000000000000000000316018032a06b8fdfdcb84790816b7af85b19305f493665fe8b4e7c51ffdd7cc144cd776a60a028a09ab55def7b8d6602ba1c97a0ebbafe64ffc9c8e89520cec97a8edfb2ebe9";
|
|
let tx = TransactionSigned::decode(&mut &hex::decode(s).unwrap()[..]).unwrap();
|
|
let mut b = Vec::new();
|
|
tx.encode(&mut b);
|
|
assert_eq!(s, hex::encode(&b));
|
|
}
|
|
|
|
proptest::proptest! {
|
|
#![proptest_config(proptest::prelude::ProptestConfig::with_cases(1))]
|
|
|
|
#[test]
|
|
fn test_parallel_recovery_order(txes in proptest::collection::vec(proptest::prelude::any::<Transaction>(), *PARALLEL_SENDER_RECOVERY_THRESHOLD * 5)) {
|
|
let mut rng =rand::thread_rng();
|
|
let secp = Secp256k1::new();
|
|
let txes: Vec<TransactionSigned> = txes.into_iter().map(|mut tx| {
|
|
if let Some(chain_id) = tx.chain_id() {
|
|
// Otherwise we might overflow when calculating `v` on `recalculate_hash`
|
|
tx.set_chain_id(chain_id % (u64::MAX / 2 - 36));
|
|
}
|
|
|
|
let key_pair = KeyPair::new(&secp, &mut rng);
|
|
|
|
let signature =
|
|
sign_message(B256::from_slice(&key_pair.secret_bytes()[..]), tx.signature_hash()).unwrap();
|
|
|
|
TransactionSigned::from_transaction_and_signature(tx, signature)
|
|
}).collect();
|
|
|
|
let parallel_senders = TransactionSigned::recover_signers(&txes, txes.len()).unwrap();
|
|
let seq_senders = txes.iter().map(|tx| tx.recover_signer()).collect::<Option<Vec<_>>>().unwrap();
|
|
|
|
assert_eq!(parallel_senders, seq_senders);
|
|
}
|
|
}
|
|
}
|