use crate::{transaction::RlpEcdsaTx, SignableTransaction, Signed, Transaction, TxType};
use alloc::vec::Vec;
use alloy_eips::{eip2930::AccessList, eip7702::SignedAuthorization};
use alloy_primitives::{
keccak256, Bytes, ChainId, PrimitiveSignature as Signature, TxKind, B256, U256,
};
use alloy_rlp::{length_of_length, BufMut, Decodable, Encodable, Header, Result};
use core::mem;
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
#[doc(alias = "LegacyTransaction", alias = "TransactionLegacy", alias = "LegacyTx")]
pub struct TxLegacy {
#[cfg_attr(
feature = "serde",
serde(
default,
with = "alloy_serde::quantity::opt",
skip_serializing_if = "Option::is_none",
)
)]
pub chain_id: Option<ChainId>,
#[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
pub nonce: u64,
#[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
pub gas_price: u128,
#[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity", rename = "gas"))]
pub gas_limit: u64,
#[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "TxKind::is_create"))]
pub to: TxKind,
pub value: U256,
pub input: Bytes,
}
impl TxLegacy {
pub const TX_TYPE: isize = 0;
#[inline]
pub fn size(&self) -> usize {
mem::size_of::<Option<ChainId>>() + mem::size_of::<u64>() + mem::size_of::<u128>() + mem::size_of::<u64>() + self.to.size() + mem::size_of::<U256>() + self.input.len() }
pub(crate) fn eip155_fields_len(&self) -> usize {
self.chain_id.map_or(
0,
|id| id.length() + 2,
)
}
pub(crate) fn encode_eip155_signing_fields(&self, out: &mut dyn BufMut) {
if let Some(id) = self.chain_id {
id.encode(out);
0x00u8.encode(out);
0x00u8.encode(out);
}
}
}
impl RlpEcdsaTx for TxLegacy {
const DEFAULT_TX_TYPE: u8 = { Self::TX_TYPE as u8 };
fn rlp_encoded_fields_length(&self) -> usize {
self.nonce.length()
+ self.gas_price.length()
+ self.gas_limit.length()
+ self.to.length()
+ self.value.length()
+ self.input.0.length()
}
fn rlp_encode_fields(&self, out: &mut dyn alloy_rlp::BufMut) {
self.nonce.encode(out);
self.gas_price.encode(out);
self.gas_limit.encode(out);
self.to.encode(out);
self.value.encode(out);
self.input.0.encode(out);
}
fn rlp_header_signed(&self, signature: &Signature) -> Header {
let payload_length = self.rlp_encoded_fields_length()
+ signature.rlp_rs_len()
+ to_eip155_value(signature.v(), self.chain_id).length();
Header { list: true, payload_length }
}
fn rlp_encoded_length_with_signature(&self, signature: &Signature) -> usize {
self.rlp_header_signed(signature).length_with_payload()
}
fn rlp_encode_signed(&self, signature: &Signature, out: &mut dyn BufMut) {
self.rlp_header_signed(signature).encode(out);
self.rlp_encode_fields(out);
signature.write_rlp_vrs(out, to_eip155_value(signature.v(), self.chain_id));
}
fn eip2718_encoded_length(&self, signature: &Signature) -> usize {
self.rlp_encoded_length_with_signature(signature)
}
fn eip2718_encode_with_type(&self, signature: &Signature, _ty: u8, out: &mut dyn BufMut) {
self.rlp_encode_signed(signature, out);
}
fn network_encoded_length(&self, signature: &Signature) -> usize {
self.rlp_encoded_length_with_signature(signature)
}
fn network_header(&self, signature: &Signature) -> Header {
self.rlp_header_signed(signature)
}
fn network_encode_with_type(&self, signature: &Signature, _ty: u8, out: &mut dyn BufMut) {
self.rlp_encode_signed(signature, out);
}
fn rlp_decode_fields(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
Ok(Self {
nonce: Decodable::decode(buf)?,
gas_price: Decodable::decode(buf)?,
gas_limit: Decodable::decode(buf)?,
to: Decodable::decode(buf)?,
value: Decodable::decode(buf)?,
input: Decodable::decode(buf)?,
chain_id: None,
})
}
fn rlp_decode_with_signature(buf: &mut &[u8]) -> alloy_rlp::Result<(Self, Signature)> {
let header = Header::decode(buf)?;
if !header.list {
return Err(alloy_rlp::Error::UnexpectedString);
}
let remaining = buf.len();
let mut tx = Self::rlp_decode_fields(buf)?;
let signature = Signature::decode_rlp_vrs(buf, |buf| {
let value = u64::decode(buf)?;
let (parity, chain_id) =
from_eip155_value(value).ok_or(alloy_rlp::Error::Custom("invalid parity value"))?;
tx.chain_id = chain_id;
Ok(parity)
})?;
if buf.len() + header.payload_length != remaining {
return Err(alloy_rlp::Error::ListLengthMismatch {
expected: header.payload_length,
got: remaining - buf.len(),
});
}
Ok((tx, signature))
}
fn eip2718_decode_with_type(
buf: &mut &[u8],
_ty: u8,
) -> alloy_eips::eip2718::Eip2718Result<Signed<Self>> {
Self::rlp_decode_signed(buf).map_err(Into::into)
}
fn eip2718_decode(buf: &mut &[u8]) -> alloy_eips::eip2718::Eip2718Result<Signed<Self>> {
Self::rlp_decode_signed(buf).map_err(Into::into)
}
fn network_decode_with_type(
buf: &mut &[u8],
_ty: u8,
) -> alloy_eips::eip2718::Eip2718Result<Signed<Self>> {
Self::rlp_decode_signed(buf).map_err(Into::into)
}
fn tx_hash_with_type(&self, signature: &Signature, _ty: u8) -> alloy_primitives::TxHash {
let mut buf = Vec::with_capacity(self.rlp_encoded_length_with_signature(signature));
self.rlp_encode_signed(signature, &mut buf);
keccak256(&buf)
}
}
impl Transaction for TxLegacy {
fn chain_id(&self) -> Option<ChainId> {
self.chain_id
}
fn nonce(&self) -> u64 {
self.nonce
}
fn gas_limit(&self) -> u64 {
self.gas_limit
}
fn gas_price(&self) -> Option<u128> {
Some(self.gas_price)
}
fn max_fee_per_gas(&self) -> u128 {
self.gas_price
}
fn max_priority_fee_per_gas(&self) -> Option<u128> {
None
}
fn max_fee_per_blob_gas(&self) -> Option<u128> {
None
}
fn priority_fee_or_price(&self) -> u128 {
self.gas_price
}
fn kind(&self) -> TxKind {
self.to
}
fn value(&self) -> U256 {
self.value
}
fn input(&self) -> &Bytes {
&self.input
}
fn ty(&self) -> u8 {
TxType::Legacy as u8
}
fn access_list(&self) -> Option<&AccessList> {
None
}
fn blob_versioned_hashes(&self) -> Option<&[B256]> {
None
}
fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
None
}
}
impl SignableTransaction<Signature> for TxLegacy {
fn set_chain_id(&mut self, chain_id: ChainId) {
self.chain_id = Some(chain_id);
}
fn encode_for_signing(&self, out: &mut dyn BufMut) {
Header {
list: true,
payload_length: self.rlp_encoded_fields_length() + self.eip155_fields_len(),
}
.encode(out);
self.rlp_encode_fields(out);
self.encode_eip155_signing_fields(out);
}
fn payload_len_for_signature(&self) -> usize {
let payload_length = self.rlp_encoded_fields_length() + self.eip155_fields_len();
Header { list: true, payload_length }.length_with_payload()
}
fn into_signed(self, signature: Signature) -> Signed<Self> {
let hash = self.tx_hash(&signature);
Signed::new_unchecked(self, signature, hash)
}
}
impl Encodable for TxLegacy {
fn encode(&self, out: &mut dyn BufMut) {
self.encode_for_signing(out)
}
fn length(&self) -> usize {
let payload_length = self.rlp_encoded_fields_length() + self.eip155_fields_len();
length_of_length(payload_length) + payload_length
}
}
impl Decodable for TxLegacy {
fn decode(data: &mut &[u8]) -> Result<Self> {
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(alloy_rlp::Error::InputTooShort);
}
let mut transaction = Self::rlp_decode_fields(data)?;
if !data.is_empty() {
transaction.chain_id = Some(Decodable::decode(data)?);
let _: U256 = Decodable::decode(data)?; let _: U256 = Decodable::decode(data)?; }
let decoded = remaining_len - data.len();
if decoded != transaction_payload_len {
return Err(alloy_rlp::Error::UnexpectedLength);
}
Ok(transaction)
}
}
pub const fn to_eip155_value(y_parity: bool, chain_id: Option<ChainId>) -> u64 {
match chain_id {
Some(id) => 35 + id * 2 + y_parity as u64,
None => 27 + y_parity as u64,
}
}
pub const fn from_eip155_value(value: u64) -> Option<(bool, Option<ChainId>)> {
match value {
27 => Some((false, None)),
28 => Some((true, None)),
v @ 35.. => Some((((v - 35) % 2) != 0, Some((v - 35) / 2))),
_ => None,
}
}
#[cfg(feature = "serde")]
pub mod signed_legacy_serde {
use super::*;
use alloc::borrow::Cow;
use alloy_primitives::U64;
use serde::{Deserialize, Serialize};
struct LegacySignature {
r: U256,
s: U256,
v: U64,
}
#[derive(Serialize, Deserialize)]
struct HumanReadableRepr {
r: U256,
s: U256,
v: U64,
}
#[derive(Serialize, Deserialize)]
#[serde(transparent)]
struct NonHumanReadableRepr((U256, U256, U64));
impl Serialize for LegacySignature {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
if serializer.is_human_readable() {
HumanReadableRepr { r: self.r, s: self.s, v: self.v }.serialize(serializer)
} else {
NonHumanReadableRepr((self.r, self.s, self.v)).serialize(serializer)
}
}
}
impl<'de> Deserialize<'de> for LegacySignature {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
if deserializer.is_human_readable() {
HumanReadableRepr::deserialize(deserializer).map(|repr| Self {
r: repr.r,
s: repr.s,
v: repr.v,
})
} else {
NonHumanReadableRepr::deserialize(deserializer).map(|repr| Self {
r: repr.0 .0,
s: repr.0 .1,
v: repr.0 .2,
})
}
}
}
#[derive(Serialize, Deserialize)]
struct SignedLegacy<'a> {
#[serde(flatten)]
tx: Cow<'a, TxLegacy>,
#[serde(flatten)]
signature: LegacySignature,
hash: B256,
}
pub fn serialize<S>(signed: &crate::Signed<TxLegacy>, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
SignedLegacy {
tx: Cow::Borrowed(signed.tx()),
signature: LegacySignature {
v: U64::from(to_eip155_value(signed.signature().v(), signed.tx().chain_id())),
r: signed.signature().r(),
s: signed.signature().s(),
},
hash: *signed.hash(),
}
.serialize(serializer)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<crate::Signed<TxLegacy>, D::Error>
where
D: serde::Deserializer<'de>,
{
let SignedLegacy { tx, signature, hash } = SignedLegacy::deserialize(deserializer)?;
let (parity, chain_id) = from_eip155_value(signature.v.to::<u64>())
.ok_or_else(|| serde::de::Error::custom("invalid EIP-155 signature parity value"))?;
if let Some(tx_chain_id) = tx.chain_id() {
if tx_chain_id > 0 && chain_id != Some(tx_chain_id) {
return Err(serde::de::Error::custom("chain id mismatch"));
}
}
let mut tx = tx.into_owned();
tx.chain_id = chain_id;
Ok(Signed::new_unchecked(tx, Signature::new(signature.r, signature.s, parity), hash))
}
}
#[cfg(all(feature = "serde", feature = "serde-bincode-compat"))]
pub(super) mod serde_bincode_compat {
use alloc::borrow::Cow;
use alloy_primitives::{Bytes, ChainId, TxKind, U256};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_with::{DeserializeAs, SerializeAs};
#[derive(Debug, Serialize, Deserialize)]
pub struct TxLegacy<'a> {
#[serde(default, with = "alloy_serde::quantity::opt")]
chain_id: Option<ChainId>,
nonce: u64,
gas_price: u128,
gas_limit: u64,
#[serde(default)]
to: TxKind,
value: U256,
input: Cow<'a, Bytes>,
}
impl<'a> From<&'a super::TxLegacy> for TxLegacy<'a> {
fn from(value: &'a super::TxLegacy) -> Self {
Self {
chain_id: value.chain_id,
nonce: value.nonce,
gas_price: value.gas_price,
gas_limit: value.gas_limit,
to: value.to,
value: value.value,
input: Cow::Borrowed(&value.input),
}
}
}
impl<'a> From<TxLegacy<'a>> for super::TxLegacy {
fn from(value: TxLegacy<'a>) -> Self {
Self {
chain_id: value.chain_id,
nonce: value.nonce,
gas_price: value.gas_price,
gas_limit: value.gas_limit,
to: value.to,
value: value.value,
input: value.input.into_owned(),
}
}
}
impl SerializeAs<super::TxLegacy> for TxLegacy<'_> {
fn serialize_as<S>(source: &super::TxLegacy, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
TxLegacy::from(source).serialize(serializer)
}
}
impl<'de> DeserializeAs<'de, super::TxLegacy> for TxLegacy<'de> {
fn deserialize_as<D>(deserializer: D) -> Result<super::TxLegacy, D::Error>
where
D: Deserializer<'de>,
{
TxLegacy::deserialize(deserializer).map(Into::into)
}
}
#[cfg(test)]
mod tests {
use arbitrary::Arbitrary;
use rand::Rng;
use serde::{Deserialize, Serialize};
use serde_with::serde_as;
use super::super::{serde_bincode_compat, TxLegacy};
#[test]
fn test_tx_legacy_bincode_roundtrip() {
#[serde_as]
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
struct Data {
#[serde_as(as = "serde_bincode_compat::TxLegacy")]
transaction: TxLegacy,
}
let mut bytes = [0u8; 1024];
rand::thread_rng().fill(bytes.as_mut_slice());
let data = Data {
transaction: TxLegacy::arbitrary(&mut arbitrary::Unstructured::new(&bytes))
.unwrap(),
};
let encoded = bincode::serialize(&data).unwrap();
let decoded: Data = bincode::deserialize(&encoded).unwrap();
assert_eq!(decoded, data);
}
}
}
#[cfg(all(test, feature = "k256"))]
mod tests {
use crate::{SignableTransaction, TxLegacy};
use alloy_primitives::{
address, b256, hex, Address, PrimitiveSignature as Signature, TxKind, B256, U256,
};
#[test]
fn recover_signer_legacy() {
let signer: Address = hex!("398137383b3d25c92898c656696e41950e47316b").into();
let hash: B256 =
hex!("bb3a336e3f823ec18197f1e13ee875700f08f03e2cab75f0d0b118dabb44cba0").into();
let tx = TxLegacy {
chain_id: Some(1),
nonce: 0x18,
gas_price: 0xfa56ea00,
gas_limit: 119902,
to: TxKind::Call(hex!("06012c8cf97bead5deae237070f9587f8e7a266d").into()),
value: U256::from(0x1c6bf526340000u64),
input: hex!("f7d8c88300000000000000000000000000000000000000000000000000000000000cee6100000000000000000000000000000000000000000000000000000000000ac3e1").into(),
};
let sig = Signature::from_scalars_and_parity(
b256!("2a378831cf81d99a3f06a18ae1b6ca366817ab4d88a70053c41d7a8f0368e031"),
b256!("450d831a05b6e418724436c05c155e0a1b7b921015d0fbc2f667aed709ac4fb5"),
false,
);
let signed_tx = tx.into_signed(sig);
assert_eq!(*signed_tx.hash(), hash, "Expected same hash");
assert_eq!(signed_tx.recover_signer().unwrap(), signer, "Recovering signer should pass.");
}
#[test]
fn decode_legacy_and_recover_signer() {
use crate::transaction::RlpEcdsaTx;
let raw_tx = alloy_primitives::bytes!("f9015482078b8505d21dba0083022ef1947a250d5630b4cf539739df2c5dacb4c659f2488d880c46549a521b13d8b8e47ff36ab50000000000000000000000000000000000000000000066ab5a608bd00a23f2fe000000000000000000000000000000000000000000000000000000000000008000000000000000000000000048c04ed5691981c42154c6167398f95e8f38a7ff00000000000000000000000000000000000000000000000000000000632ceac70000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006c6ee5e31d828de241282b9606c8e98ea48526e225a0c9077369501641a92ef7399ff81c21639ed4fd8fc69cb793cfa1dbfab342e10aa0615facb2f1bcf3274a354cfe384a38d0cc008a11c2dd23a69111bc6930ba27a8");
let tx = TxLegacy::rlp_decode_signed(&mut raw_tx.as_ref()).unwrap();
let recovered = tx.recover_signer().unwrap();
let expected = address!("a12e1462d0ceD572f396F58B6E2D03894cD7C8a4");
assert_eq!(tx.tx().chain_id, Some(1), "Expected same chain id");
assert_eq!(expected, recovered, "Expected same signer");
}
}