From 01544ec4b70317330836742439336b87bca10604 Mon Sep 17 00:00:00 2001 From: Dan Cline <6798349+Rjected@users.noreply.github.com> Date: Sun, 30 Jan 2022 14:21:16 -0500 Subject: [PATCH] Implement RLP decoding for transactions (#805) * Implement RLP decoding for transactions * set chain_id in fill_transaction --- ethers-core/src/types/ens.rs | 39 ++- ethers-core/src/types/transaction/eip1559.rs | 72 +++++- ethers-core/src/types/transaction/eip2718.rs | 169 +++++++++++- ethers-core/src/types/transaction/eip2930.rs | 74 +++++- ethers-core/src/types/transaction/mod.rs | 41 +++ ethers-core/src/types/transaction/request.rs | 237 ++++++++++++++++- ethers-core/src/types/transaction/response.rs | 240 +++++++++++++++++- ethers-middleware/src/signer.rs | 26 +- ethers-middleware/tests/nonce_manager.rs | 5 +- ethers-middleware/tests/signer.rs | 2 +- ethers-providers/src/lib.rs | 4 +- ethers-signers/src/aws/mod.rs | 2 +- ethers-signers/src/ledger/app.rs | 2 +- ethers-signers/src/wallet/mod.rs | 22 +- ethers-signers/src/wallet/private_key.rs | 15 +- 15 files changed, 889 insertions(+), 61 deletions(-) diff --git a/ethers-core/src/types/ens.rs b/ethers-core/src/types/ens.rs index 35784a8b..9b68ac4e 100644 --- a/ethers-core/src/types/ens.rs +++ b/ethers-core/src/types/ens.rs @@ -1,5 +1,7 @@ +use std::cmp::Ordering; + use crate::types::Address; -use rlp::{Encodable, RlpStream}; +use rlp::{Decodable, Encodable, RlpStream}; use serde::{ser::Error as SerializationError, Deserialize, Deserializer, Serialize, Serializer}; /// ENS name or Ethereum Address. Not RLP encoded/serialized if it's a name @@ -29,6 +31,25 @@ impl Encodable for NameOrAddress { } } +impl Decodable for NameOrAddress { + fn decode(rlp: &rlp::Rlp) -> Result { + // An address (H160) is 20 bytes, so let's only accept 20 byte rlp string encodings. + if !rlp.is_data() { + return Err(rlp::DecoderError::RlpExpectedToBeData) + } + + // the data needs to be 20 bytes long + match 20.cmp(&rlp.size()) { + Ordering::Less => Err(rlp::DecoderError::RlpIsTooShort), + Ordering::Greater => Err(rlp::DecoderError::RlpIsTooBig), + Ordering::Equal => { + let rlp_data = rlp.data()?; + Ok(NameOrAddress::Address(Address::from_slice(rlp_data))) + } + } + } +} + impl From<&str> for NameOrAddress { fn from(s: &str) -> Self { NameOrAddress::Name(s.to_owned()) @@ -71,6 +92,8 @@ impl<'de> Deserialize<'de> for NameOrAddress { #[cfg(test)] mod tests { + use rlp::Rlp; + use super::*; #[test] @@ -103,6 +126,20 @@ mod tests { assert_eq!(rlp.as_raw(), expected.as_raw()); } + #[test] + fn rlp_address_deserialized() { + let addr = "3dd6f334b732d23b51dfbee2070b40bbd1a97a8f".parse().unwrap(); + let expected = NameOrAddress::Address(addr); + + let mut rlp = RlpStream::new(); + rlp.append(&addr); + let rlp_bytes = &rlp.out().freeze()[..]; + let data = Rlp::new(rlp_bytes); + let name = NameOrAddress::decode(&data).unwrap(); + + assert_eq!(name, expected); + } + #[test] fn serde_name_not_serialized() { let name = NameOrAddress::Name("ens.eth".to_string()); diff --git a/ethers-core/src/types/transaction/eip1559.rs b/ethers-core/src/types/transaction/eip1559.rs index 2868be31..38d1d705 100644 --- a/ethers-core/src/types/transaction/eip1559.rs +++ b/ethers-core/src/types/transaction/eip1559.rs @@ -3,7 +3,7 @@ use crate::{ types::{Address, Bytes, NameOrAddress, Signature, H256, U256, U64}, utils::keccak256, }; -use rlp::RlpStream; +use rlp::{Decodable, DecoderError, RlpStream}; /// EIP-1559 transactions have 9 fields const NUM_TX_FIELDS: usize = 9; @@ -57,6 +57,10 @@ pub struct Eip1559TransactionRequest { /// baseFeePerGas and maxPriorityFeePerGas). The difference between maxFeePerGas and /// baseFeePerGas + maxPriorityFeePerGas is “refunded” to the user. pub max_fee_per_gas: Option, + + #[serde(rename = "chainId", default, skip_serializing_if = "Option::is_none")] + /// Chain ID (None for mainnet) + pub chain_id: Option, } impl Eip1559TransactionRequest { @@ -130,25 +134,34 @@ impl Eip1559TransactionRequest { self } + /// Sets the `chain_id` field in the transaction to the provided value + #[must_use] + pub fn chain_id>(mut self, chain_id: T) -> Self { + self.chain_id = Some(chain_id.into()); + self + } + /// Hashes the transaction's data with the provided chain id - pub fn sighash>(&self, chain_id: T) -> H256 { - keccak256(self.rlp(chain_id).as_ref()).into() + pub fn sighash(&self) -> H256 { + keccak256(self.rlp().as_ref()).into() } /// Gets the unsigned transaction's RLP encoding - pub fn rlp>(&self, chain_id: T) -> Bytes { + pub fn rlp(&self) -> Bytes { let mut rlp = RlpStream::new(); rlp.begin_list(NUM_TX_FIELDS); - self.rlp_base(chain_id, &mut rlp); + self.rlp_base(&mut rlp); rlp.out().freeze().into() } /// Produces the RLP encoding of the transaction with the provided signature - pub fn rlp_signed>(&self, chain_id: T, signature: &Signature) -> Bytes { + pub fn rlp_signed(&self, signature: &Signature) -> Bytes { let mut rlp = RlpStream::new(); rlp.begin_unbounded_list(); - let chain_id = chain_id.into(); - self.rlp_base(chain_id, &mut rlp); + self.rlp_base(&mut rlp); + + // if the chain_id is none we assume mainnet and choose one + let chain_id = self.chain_id.unwrap_or_else(U64::one); // append the signature let v = normalize_v(signature.v, chain_id); @@ -159,8 +172,8 @@ impl Eip1559TransactionRequest { rlp.out().freeze().into() } - pub(crate) fn rlp_base>(&self, chain_id: T, rlp: &mut RlpStream) { - rlp.append(&chain_id.into()); + pub(crate) fn rlp_base(&self, rlp: &mut RlpStream) { + rlp_opt(rlp, &self.chain_id); rlp_opt(rlp, &self.nonce); rlp_opt(rlp, &self.max_priority_fee_per_gas); rlp_opt(rlp, &self.max_fee_per_gas); @@ -170,6 +183,44 @@ impl Eip1559TransactionRequest { rlp_opt(rlp, &self.data.as_ref().map(|d| d.as_ref())); rlp.append(&self.access_list); } + + /// Decodes fields of the request starting at the RLP offset passed. Increments the offset for + /// each element parsed. + #[inline] + fn decode_base_rlp(&mut self, rlp: &rlp::Rlp, offset: &mut usize) -> Result<(), DecoderError> { + self.chain_id = Some(rlp.val_at(*offset)?); + *offset += 1; + self.nonce = Some(rlp.val_at(*offset)?); + *offset += 1; + self.max_priority_fee_per_gas = Some(rlp.val_at(*offset)?); + *offset += 1; + self.max_fee_per_gas = Some(rlp.val_at(*offset)?); + *offset += 1; + self.gas = Some(rlp.val_at(*offset)?); + *offset += 1; + self.to = Some(rlp.val_at(*offset)?); + *offset += 1; + self.value = Some(rlp.val_at(*offset)?); + *offset += 1; + let data = rlp::Rlp::new(rlp.at(*offset)?.as_raw()).data()?; + self.data = match data.len() { + 0 => None, + _ => Some(Bytes::from(data.to_vec())), + }; + *offset += 1; + self.access_list = rlp.val_at(*offset)?; + *offset += 1; + Ok(()) + } +} + +impl Decodable for Eip1559TransactionRequest { + fn decode(rlp: &rlp::Rlp) -> Result { + let mut txn = Eip1559TransactionRequest::new(); + let mut offset = 0; + txn.decode_base_rlp(rlp, &mut offset)?; + Ok(txn) + } } impl From for super::request::TransactionRequest { @@ -188,6 +239,7 @@ impl From for super::request::TransactionRequest { gateway_fee_recipient: None, #[cfg(feature = "celo")] gateway_fee: None, + chain_id: tx.chain_id, } } } diff --git a/ethers-core/src/types/transaction/eip2718.rs b/ethers-core/src/types/transaction/eip2718.rs index 60db1fb6..fe211df7 100644 --- a/ethers-core/src/types/transaction/eip2718.rs +++ b/ethers-core/src/types/transaction/eip2718.rs @@ -162,6 +162,23 @@ impl TypedTransaction { }; } + pub fn chain_id(&self) -> Option { + match self { + Legacy(inner) => inner.chain_id, + Eip2930(inner) => inner.tx.chain_id, + Eip1559(inner) => inner.chain_id, + } + } + + pub fn set_chain_id>(&mut self, chain_id: T) { + let chain_id = chain_id.into(); + match self { + Legacy(inner) => inner.chain_id = Some(chain_id), + Eip2930(inner) => inner.tx.chain_id = Some(chain_id), + Eip1559(inner) => inner.chain_id = Some(chain_id), + }; + } + pub fn data(&self) -> Option<&Bytes> { match self { Legacy(inner) => inner.data.as_ref(), @@ -194,7 +211,7 @@ impl TypedTransaction { }; } - pub fn rlp_signed>(&self, chain_id: T, signature: &Signature) -> Bytes { + pub fn rlp_signed(&self, signature: &Signature) -> Bytes { let mut encoded = vec![]; match self { Legacy(ref tx) => { @@ -202,44 +219,71 @@ impl TypedTransaction { } Eip2930(inner) => { encoded.extend_from_slice(&[0x1]); - encoded.extend_from_slice(inner.rlp_signed(chain_id, signature).as_ref()); + encoded.extend_from_slice(inner.rlp_signed(signature).as_ref()); } Eip1559(inner) => { encoded.extend_from_slice(&[0x2]); - encoded.extend_from_slice(inner.rlp_signed(chain_id, signature).as_ref()); + encoded.extend_from_slice(inner.rlp_signed(signature).as_ref()); } }; encoded.into() } - pub fn rlp>(&self, chain_id: T) -> Bytes { - let chain_id = chain_id.into(); + pub fn rlp(&self) -> Bytes { let mut encoded = vec![]; match self { Legacy(inner) => { - encoded.extend_from_slice(inner.rlp(chain_id).as_ref()); + encoded.extend_from_slice(inner.rlp().as_ref()); } Eip2930(inner) => { encoded.extend_from_slice(&[0x1]); - encoded.extend_from_slice(inner.rlp(chain_id).as_ref()); + encoded.extend_from_slice(inner.rlp().as_ref()); } Eip1559(inner) => { encoded.extend_from_slice(&[0x2]); - encoded.extend_from_slice(inner.rlp(chain_id).as_ref()); + encoded.extend_from_slice(inner.rlp().as_ref()); } }; encoded.into() } - /// Hashes the transaction's data with the provided chain id - /// Does not double-RLP encode - pub fn sighash>(&self, chain_id: T) -> H256 { - let encoded = self.rlp(chain_id); + /// Hashes the transaction's data. Does not double-RLP encode + pub fn sighash(&self) -> H256 { + let encoded = self.rlp(); keccak256(encoded).into() } } +/// Get a TypedTransaction directly from an rlp encoded byte stream +impl rlp::Decodable for TypedTransaction { + fn decode(rlp: &rlp::Rlp) -> Result { + let tx_type: Option = match rlp.is_data() { + true => Some(rlp.data().unwrap().into()), + false => None, + }; + let rest = rlp::Rlp::new( + rlp.as_raw().get(1..).ok_or(rlp::DecoderError::Custom("no transaction payload"))?, + ); + + match tx_type { + Some(x) if x == U64::from(1) => { + // EIP-2930 (0x01) + Ok(Self::Eip2930(Eip2930TransactionRequest::decode(&rest)?)) + } + Some(x) if x == U64::from(2) => { + // EIP-1559 (0x02) + Ok(Self::Eip1559(Eip1559TransactionRequest::decode(&rest)?)) + } + _ => { + // Legacy (0x00) + // use the original rlp + Ok(Self::Legacy(TransactionRequest::decode(rlp)?)) + } + } + } +} + impl From for TypedTransaction { fn from(src: TransactionRequest) -> TypedTransaction { TypedTransaction::Legacy(src) @@ -260,8 +304,11 @@ impl From for TypedTransaction { #[cfg(test)] mod tests { + use rlp::Decodable; + use super::*; use crate::types::{Address, U256}; + use std::str::FromStr; #[test] fn serde_legacy_tx() { @@ -276,4 +323,102 @@ mod tests { let de: TransactionRequest = serde_json::from_str(&serialized).unwrap(); assert_eq!(tx, TypedTransaction::Legacy(de)); } + + #[test] + fn test_typed_tx_without_access_list() { + let tx: Eip1559TransactionRequest = serde_json::from_str( + r#"{ + "gas": "0x186a0", + "maxFeePerGas": "0x77359400", + "maxPriorityFeePerGas": "0x77359400", + "data": "0x5544", + "nonce": "0x2", + "to": "0x96216849c49358B10257cb55b28eA603c874b05E", + "value": "0x5af3107a4000", + "type": "0x2", + "chainId": "0x539", + "accessList": [], + "v": "0x1", + "r": "0xc3000cd391f991169ebfd5d3b9e93c89d31a61c998a21b07a11dc6b9d66f8a8e", + "s": "0x22cfe8424b2fbd78b16c9911da1be2349027b0a3c40adf4b6459222323773f74" + }"#, + ) + .unwrap(); + + let envelope = TypedTransaction::Eip1559(tx); + + let expected = + H256::from_str("0xa1ea3121940930f7e7b54506d80717f14c5163807951624c36354202a8bffda6") + .unwrap(); + let actual = envelope.sighash(); + assert_eq!(expected, actual); + } + + #[test] + fn test_typed_tx() { + let tx: Eip1559TransactionRequest = serde_json::from_str( + r#"{ + "gas": "0x186a0", + "maxFeePerGas": "0x77359400", + "maxPriorityFeePerGas": "0x77359400", + "data": "0x5544", + "nonce": "0x2", + "to": "0x96216849c49358B10257cb55b28eA603c874b05E", + "value": "0x5af3107a4000", + "type": "0x2", + "accessList": [ + { + "address": "0x0000000000000000000000000000000000000001", + "storageKeys": [ + "0x0100000000000000000000000000000000000000000000000000000000000000" + ] + } + ], + "chainId": "0x539", + "v": "0x1", + "r": "0xc3000cd391f991169ebfd5d3b9e93c89d31a61c998a21b07a11dc6b9d66f8a8e", + "s": "0x22cfe8424b2fbd78b16c9911da1be2349027b0a3c40adf4b6459222323773f74" + }"#, + ) + .unwrap(); + + let envelope = TypedTransaction::Eip1559(tx); + + let expected = + H256::from_str("0x090b19818d9d087a49c3d2ecee4829ee4acea46089c1381ac5e588188627466d") + .unwrap(); + let actual = envelope.sighash(); + assert_eq!(expected, actual); + } + + #[test] + fn test_typed_tx_decode() { + // this is the same transaction as the above test + let typed_tx_hex = hex::decode("02f86b8205390284773594008477359400830186a09496216849c49358b10257cb55b28ea603c874b05e865af3107a4000825544f838f7940000000000000000000000000000000000000001e1a00100000000000000000000000000000000000000000000000000000000000000").unwrap(); + let tx_rlp = rlp::Rlp::new(typed_tx_hex.as_slice()); + let actual_tx = TypedTransaction::decode(&tx_rlp).unwrap(); + + let expected = + H256::from_str("0x090b19818d9d087a49c3d2ecee4829ee4acea46089c1381ac5e588188627466d") + .unwrap(); + let actual = actual_tx.sighash(); + assert_eq!(expected, actual); + } + + #[cfg(not(feature = "celo"))] + #[test] + fn test_eip155_decode() { + let tx = TransactionRequest::new() + .nonce(9) + .to("3535353535353535353535353535353535353535".parse::
().unwrap()) + .value(1000000000000000000u64) + .gas_price(20000000000u64) + .gas(21000) + .chain_id(1); + + let expected_hex = hex::decode("ec098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a764000080018080").unwrap(); + let expected_rlp = rlp::Rlp::new(expected_hex.as_slice()); + let decoded_transaction = TypedTransaction::decode(&expected_rlp).unwrap(); + assert_eq!(tx.sighash(), decoded_transaction.sighash()); + } } diff --git a/ethers-core/src/types/transaction/eip2930.rs b/ethers-core/src/types/transaction/eip2930.rs index c9b6fb4e..5e78b75f 100644 --- a/ethers-core/src/types/transaction/eip2930.rs +++ b/ethers-core/src/types/transaction/eip2930.rs @@ -1,8 +1,8 @@ use super::{normalize_v, request::TransactionRequest}; use crate::types::{Address, Bytes, Signature, H256, U256, U64}; -use rlp::RlpStream; -use rlp_derive::{RlpEncodable, RlpEncodableWrapper}; +use rlp::{Decodable, DecoderError, RlpStream}; +use rlp_derive::{RlpDecodable, RlpDecodableWrapper, RlpEncodable, RlpEncodableWrapper}; use serde::{Deserialize, Serialize}; const NUM_EIP2930_FIELDS: usize = 8; @@ -10,7 +10,17 @@ const NUM_EIP2930_FIELDS: usize = 8; /// Access list // NB: Need to use `RlpEncodableWrapper` else we get an extra [] in the output // https://github.com/gakonst/ethers-rs/pull/353#discussion_r680683869 -#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, RlpEncodableWrapper)] +#[derive( + Debug, + Default, + Clone, + PartialEq, + Eq, + Serialize, + Deserialize, + RlpEncodableWrapper, + RlpDecodableWrapper, +)] pub struct AccessList(pub Vec); #[derive(Serialize, Deserialize, Clone, Debug)] @@ -38,7 +48,9 @@ impl TransactionRequest { } /// Access list item -#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, RlpEncodable)] +#[derive( + Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, RlpEncodable, RlpDecodable, +)] #[serde(rename_all = "camelCase")] pub struct AccessListItem { /// Accessed address @@ -60,10 +72,12 @@ impl Eip2930TransactionRequest { Self { tx, access_list } } - pub fn rlp>(&self, chain_id: T) -> Bytes { + pub fn rlp(&self) -> Bytes { let mut rlp = RlpStream::new(); rlp.begin_list(NUM_EIP2930_FIELDS); - rlp.append(&chain_id.into()); + + let chain_id = self.tx.chain_id.unwrap_or_else(U64::one); + rlp.append(&chain_id); self.tx.rlp_base(&mut rlp); // append the access list in addition to the base rlp encoding rlp.append(&self.access_list); @@ -72,11 +86,11 @@ impl Eip2930TransactionRequest { } /// Produces the RLP encoding of the transaction with the provided signature - pub fn rlp_signed>(&self, chain_id: T, signature: &Signature) -> Bytes { + pub fn rlp_signed(&self, signature: &Signature) -> Bytes { let mut rlp = RlpStream::new(); rlp.begin_list(NUM_EIP2930_FIELDS + 3); - let chain_id = chain_id.into(); + let chain_id = self.tx.chain_id.unwrap_or_else(U64::one); rlp.append(&chain_id); self.tx.rlp_base(&mut rlp); // append the access list in addition to the base rlp encoding @@ -89,10 +103,50 @@ impl Eip2930TransactionRequest { rlp.append(&signature.s); rlp.out().freeze().into() } + + /// Decodes fields based on the RLP offset passed + fn decode_base_rlp(&mut self, rlp: &rlp::Rlp, offset: &mut usize) -> Result<(), DecoderError> { + self.tx.chain_id = Some(rlp.val_at(*offset)?); + *offset += 1; + self.tx.nonce = Some(rlp.val_at(*offset)?); + *offset += 1; + self.tx.gas_price = Some(rlp.val_at(*offset)?); + *offset += 1; + self.tx.gas = Some(rlp.val_at(*offset)?); + *offset += 1; + + self.tx.gas = Some(rlp.val_at(*offset)?); + *offset += 1; + self.tx.to = Some(rlp.val_at(*offset)?); + *offset += 1; + self.tx.value = Some(rlp.val_at(*offset)?); + *offset += 1; + let data = rlp::Rlp::new(rlp.at(*offset)?.as_raw()).data()?; + self.tx.data = match data.len() { + 0 => None, + _ => Some(Bytes::from(data.to_vec())), + }; + *offset += 1; + self.access_list = rlp.val_at(*offset)?; + *offset += 1; + + Ok(()) + } +} + +/// Get a Eip2930TransactionRequest from a rlp encoded byte stream +impl Decodable for Eip2930TransactionRequest { + fn decode(rlp: &rlp::Rlp) -> Result { + let mut new_tx = Self::new(TransactionRequest::new(), AccessList::default()); + let mut offset = 0; + new_tx.decode_base_rlp(rlp, &mut offset)?; + Ok(new_tx) + } } #[cfg(test)] mod tests { + use super::*; use crate::types::{transaction::eip2718::TypedTransaction, U256}; @@ -110,14 +164,14 @@ mod tests { .with_access_list(vec![]) .into(); - let hash = tx.sighash(1); + let hash = tx.sighash(); let sig: Signature = "c9519f4f2b30335884581971573fadf60c6204f59a911df35ee8a540456b266032f1e8e2c5dd761f9e4f88f41c8310aeaba26a8bfcdacfedfa12ec3862d3752101".parse().unwrap(); assert_eq!( hash, "49b486f0ec0a60dfbbca2d30cb07c9e8ffb2a2ff41f29a1ab6737475f6ff69f3".parse().unwrap() ); - let enc = rlp::encode(&tx.rlp_signed(1, &sig).as_ref()); + let enc = rlp::encode(&tx.rlp_signed(&sig).as_ref()); let expected = "b86601f8630103018261a894b94f5374fce5edbc8e2a8697c15331677e6ebf0b0a825544c001a0c9519f4f2b30335884581971573fadf60c6204f59a911df35ee8a540456b2660a032f1e8e2c5dd761f9e4f88f41c8310aeaba26a8bfcdacfedfa12ec3862d37521"; assert_eq!(hex::encode(&enc), expected); } diff --git a/ethers-core/src/types/transaction/mod.rs b/ethers-core/src/types/transaction/mod.rs index baf9ce17..b4a25e97 100644 --- a/ethers-core/src/types/transaction/mod.rs +++ b/ethers-core/src/types/transaction/mod.rs @@ -33,3 +33,44 @@ pub(crate) fn normalize_v(v: u64, chain_id: crate::types::U64) -> u64 { v } } + +/// extracts the chainid from the signature v value based on EIP-155 +pub(crate) fn extract_chain_id(v: u64) -> Option { + // https://eips.ethereum.org/EIPS/eip-155 + // if chainid is available, v = {0, 1} + CHAIN_ID * 2 + 35 + if v >= 35 { + return Some(crate::types::U64::from((v - 35) >> 1)) + } + None +} + +/// Decodes the signature portion of the RLP encoding based on the RLP offset passed. +/// Increments the offset for each element parsed. +#[inline] +fn decode_signature( + rlp: &rlp::Rlp, + offset: &mut usize, +) -> Result { + let sig = super::Signature { + v: rlp.val_at(*offset)?, + r: rlp.val_at(*offset + 1)?, + s: rlp.val_at(*offset + 2)?, + }; + *offset += 3; + Ok(sig) +} + +#[cfg(test)] +mod tests { + use crate::types::{transaction::rlp_opt, U64}; + use rlp::RlpStream; + + #[test] + fn test_rlp_opt_none() { + let mut stream = RlpStream::new_list(1); + let empty_chainid: Option = None; + rlp_opt(&mut stream, &empty_chainid); + let out = stream.out(); + assert_eq!(out, vec![0xc1, 0x80]); + } +} diff --git a/ethers-core/src/types/transaction/request.rs b/ethers-core/src/types/transaction/request.rs index 371b120f..12f579cc 100644 --- a/ethers-core/src/types/transaction/request.rs +++ b/ethers-core/src/types/transaction/request.rs @@ -1,11 +1,11 @@ //! Transaction types -use super::{rlp_opt, NUM_TX_FIELDS}; +use super::{extract_chain_id, rlp_opt, NUM_TX_FIELDS}; use crate::{ types::{Address, Bytes, NameOrAddress, Signature, H256, U256, U64}, utils::keccak256, }; -use rlp::RlpStream; +use rlp::{Decodable, RlpStream}; use serde::{Deserialize, Serialize}; /// Parameters for sending a transaction @@ -41,6 +41,10 @@ pub struct TransactionRequest { #[serde(skip_serializing_if = "Option::is_none")] pub nonce: Option, + /// Chain ID (None for mainnet) + #[serde(skip_serializing_if = "Option::is_none")] + pub chain_id: Option, + ///////////////// Celo-specific transaction fields ///////////////// /// The currency fees are paid in (None for native currency) #[cfg(feature = "celo")] @@ -123,29 +127,49 @@ impl TransactionRequest { self } - /// Hashes the transaction's data with the provided chain id - pub fn sighash>(&self, chain_id: T) -> H256 { - keccak256(self.rlp(chain_id).as_ref()).into() + /// Sets the `chain_id` field in the transaction to the provided value + #[must_use] + pub fn chain_id>(mut self, chain_id: T) -> Self { + self.chain_id = Some(chain_id.into()); + self } - /// Gets the unsigned transaction's RLP encoding - pub fn rlp>(&self, chain_id: T) -> Bytes { + /// Hashes the transaction's data with the provided chain id + pub fn sighash(&self) -> H256 { + match self.chain_id { + Some(_) => keccak256(self.rlp().as_ref()).into(), + None => keccak256(self.rlp_unsigned().as_ref()).into(), + } + } + + /// Gets the transaction's RLP encoding, prepared with the chain_id and extra fields for + /// signing. Assumes the chainid exists. + pub fn rlp(&self) -> Bytes { let mut rlp = RlpStream::new(); rlp.begin_list(NUM_TX_FIELDS); self.rlp_base(&mut rlp); // Only hash the 3 extra fields when preparing the // data to sign if chain_id is present - rlp.append(&chain_id.into()); + rlp_opt(&mut rlp, &self.chain_id); rlp.append(&0u8); rlp.append(&0u8); rlp.out().freeze().into() } + /// Gets the unsigned transaction's RLP encoding + pub fn rlp_unsigned(&self) -> Bytes { + let mut rlp = RlpStream::new(); + rlp.begin_list(NUM_TX_FIELDS - 3); + self.rlp_base(&mut rlp); + rlp.out().freeze().into() + } + /// Produces the RLP encoding of the transaction with the provided signature pub fn rlp_signed(&self, signature: &Signature) -> Bytes { let mut rlp = RlpStream::new(); rlp.begin_list(NUM_TX_FIELDS); + self.rlp_base(&mut rlp); // append the signature @@ -167,6 +191,86 @@ impl TransactionRequest { rlp_opt(rlp, &self.value); rlp_opt(rlp, &self.data.as_ref().map(|d| d.as_ref())); } + + /// Decodes the unsigned rlp, returning the transaction request and incrementing the counter + /// passed as we are traversing the rlp list. + fn decode_unsigned_rlp_base( + rlp: &rlp::Rlp, + offset: &mut usize, + ) -> Result { + let mut txn = TransactionRequest::new(); + txn.nonce = Some(rlp.at(*offset)?.as_val()?); + *offset += 1; + txn.gas_price = Some(rlp.at(*offset)?.as_val()?); + *offset += 1; + txn.gas = Some(rlp.at(*offset)?.as_val()?); + *offset += 1; + + #[cfg(feature = "celo")] + { + txn.fee_currency = Some(rlp.at(*offset)?.as_val()?); + *offset += 1; + txn.gateway_fee_recipient = Some(rlp.at(*offset)?.as_val()?); + *offset += 1; + txn.gateway_fee = Some(rlp.at(*offset)?.as_val()?); + *offset += 1; + } + + txn.to = Some(rlp.at(*offset)?.as_val()?); + *offset += 1; + txn.value = Some(rlp.at(*offset)?.as_val()?); + *offset += 1; + + // finally we need to extract the data which will be encoded as another rlp + let txndata = rlp::Rlp::new(rlp.at(*offset)?.as_raw()).data()?; + txn.data = match txndata.len() { + 0 => None, + _ => Some(Bytes::from(txndata.to_vec())), + }; + *offset += 1; + Ok(txn) + } + + /// Decodes RLP into a transaction. + pub fn decode_unsigned_rlp(rlp: &rlp::Rlp) -> Result { + let mut offset = 0; + let mut txn = Self::decode_unsigned_rlp_base(rlp, &mut offset)?; + + // If a signed transaction is passed to this method, the chainid would be set to the v value + // of the signature. + if let Ok(chainid) = rlp.at(offset)?.as_val() { + txn.chain_id = Some(chainid); + } + + // parse the last two elements so we return an error if a signed transaction is passed + let _first_zero: u8 = rlp.at(offset + 1)?.as_val()?; + let _second_zero: u8 = rlp.at(offset + 2)?.as_val()?; + Ok(txn) + } + + /// Decodes the given RLP into a transaction, attempting to decode its signature as well. + pub fn decode_signed_rlp(rlp: &rlp::Rlp) -> Result<(Self, Signature), rlp::DecoderError> { + let mut offset = 0; + let mut txn = Self::decode_unsigned_rlp_base(rlp, &mut offset)?; + + let v = rlp.at(offset)?.as_val()?; + // populate chainid from v + txn.chain_id = extract_chain_id(v); + offset += 1; + let r = rlp.at(offset)?.as_val()?; + offset += 1; + let s = rlp.at(offset)?.as_val()?; + + let sig = Signature { r, s, v }; + Ok((txn, sig)) + } +} + +impl Decodable for TransactionRequest { + /// Decodes the given RLP into a transaction request, ignoring the signature if populated + fn decode(rlp: &rlp::Rlp) -> Result { + TransactionRequest::decode_unsigned_rlp(rlp) + } } // Separate impl block for the celo-specific fields @@ -207,8 +311,46 @@ impl TransactionRequest { #[cfg(test)] #[cfg(not(feature = "celo"))] mod tests { - use super::*; + use crate::types::Signature; + use rlp::{Decodable, Rlp}; + use super::{Address, TransactionRequest, U256, U64}; + + #[test] + fn encode_decode_rlp() { + let tx = TransactionRequest::new() + .nonce(3) + .gas_price(1) + .gas(25000) + .to("b94f5374fce5edbc8e2a8697c15331677e6ebf0b".parse::
().unwrap()) + .value(10) + .data(vec![0x55, 0x44]) + .chain_id(1); + + // turn the rlp bytes encoding into a rlp stream and check that the decoding returns the + // same struct + let rlp_bytes = &tx.rlp().to_vec()[..]; + let got_rlp = Rlp::new(rlp_bytes); + let txn_request = TransactionRequest::decode(&got_rlp).unwrap(); + + // We compare the sighash rather than the specific struct + assert_eq!(tx.sighash(), txn_request.sighash()); + } + + #[test] + // test data from https://github.com/ethereum/go-ethereum/blob/b1e72f7ea998ad662166bcf23705ca59cf81e925/core/types/transaction_test.go#L40 + fn empty_sighash_check() { + let tx = TransactionRequest::new() + .nonce(0) + .to("095e7baea6a6c7c4c2dfeb977efac326af552d87".parse::
().unwrap()) + .value(0) + .gas(0) + .gas_price(0); + + let expected_sighash = "c775b99e7ad12f50d819fcd602390467e28141316969f4b57f0626f74fe3b386"; + let got_sighash = hex::encode(tx.sighash().as_bytes()); + assert_eq!(expected_sighash, got_sighash); + } #[test] fn decode_unsigned_transaction() { let _res: TransactionRequest = serde_json::from_str( @@ -226,4 +368,81 @@ mod tests { ) .unwrap(); } + + #[test] + fn decode_known_rlp_goerli() { + let tx = TransactionRequest::new() + .nonce(70272) + .from("fab2b4b677a4e104759d378ea25504862150256e".parse::
().unwrap()) + .to("d1f23226fb4d2b7d2f3bcdd99381b038de705a64".parse::
().unwrap()) + .value(0) + .gas_price(1940000007) + .gas(21000); + + let expected_rlp = hex::decode("f866830112808473a20d0782520894d1f23226fb4d2b7d2f3bcdd99381b038de705a6480801ca04bc89d41c954168afb4cbd01fe2e0f9fe12e3aa4665eefcee8c4a208df044b5da05d410fd85a2e31870ea6d6af53fafc8e3c1ae1859717c863cac5cff40fee8da4").unwrap(); + let (got_tx, _signature) = + TransactionRequest::decode_signed_rlp(&Rlp::new(&expected_rlp)).unwrap(); + + // intialization of TransactionRequests using new() uses the Default trait, so we just + // compare the sighash and signed encoding instead. + assert_eq!(got_tx.sighash(), tx.sighash()); + } + + #[test] + fn test_eip155_encode() { + let tx = TransactionRequest::new() + .nonce(9) + .to("3535353535353535353535353535353535353535".parse::
().unwrap()) + .value(1000000000000000000u64) + .gas_price(20000000000u64) + .gas(21000) + .chain_id(1); + + let expected_rlp = hex::decode("ec098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a764000080018080").unwrap(); + assert_eq!(expected_rlp, tx.rlp().to_vec()); + + let expected_sighash = + hex::decode("daf5a779ae972f972197303d7b574746c7ef83eadac0f2791ad23db92e4c8e53") + .unwrap(); + + assert_eq!(expected_sighash, tx.sighash().as_bytes().to_vec()); + } + + #[test] + fn test_eip155_decode() { + let tx = TransactionRequest::new() + .nonce(9) + .to("3535353535353535353535353535353535353535".parse::
().unwrap()) + .value(1000000000000000000u64) + .gas_price(20000000000u64) + .gas(21000) + .chain_id(1); + + let expected_hex = hex::decode("ec098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a764000080018080").unwrap(); + let expected_rlp = rlp::Rlp::new(expected_hex.as_slice()); + let decoded_transaction = TransactionRequest::decode(&expected_rlp).unwrap(); + assert_eq!(tx, decoded_transaction); + } + + #[test] + fn test_eip155_decode_signed() { + let expected_signed_bytes = hex::decode("f86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83").unwrap(); + let expected_signed_rlp = rlp::Rlp::new(expected_signed_bytes.as_slice()); + let (decoded_tx, decoded_sig) = + TransactionRequest::decode_signed_rlp(&expected_signed_rlp).unwrap(); + + let expected_sig = Signature { + v: 37, + r: U256::from_dec_str( + "18515461264373351373200002665853028612451056578545711640558177340181847433846", + ) + .unwrap(), + s: U256::from_dec_str( + "46948507304638947509940763649030358759909902576025900602547168820602576006531", + ) + .unwrap(), + }; + assert_eq!(expected_sig, decoded_sig); + assert_eq!(decoded_tx.chain_id, Some(U64::from(1))); + } } diff --git a/ethers-core/src/types/transaction/response.rs b/ethers-core/src/types/transaction/response.rs index d5eb190b..a13f1f5a 100644 --- a/ethers-core/src/types/transaction/response.rs +++ b/ethers-core/src/types/transaction/response.rs @@ -1,10 +1,10 @@ //! Transaction types -use super::{eip2930::AccessList, normalize_v, rlp_opt}; +use super::{decode_signature, eip2930::AccessList, normalize_v, rlp_opt}; use crate::{ types::{Address, Bloom, Bytes, Log, H256, U256, U64}, utils::keccak256, }; -use rlp::RlpStream; +use rlp::{Decodable, DecoderError, RlpStream}; use serde::{Deserialize, Serialize}; /// Details of a signed transaction @@ -201,6 +201,152 @@ impl Transaction { _ => rlp_bytes, } } + + /// Decodes the Celo-specific metadata starting at the RLP offset passed. + /// Increments the offset for each element parsed. + #[cfg(feature = "celo")] + #[inline] + fn decode_celo_metadata( + &mut self, + rlp: &rlp::Rlp, + offset: &mut usize, + ) -> Result<(), DecoderError> { + self.fee_currency = Some(rlp.val_at(*offset)?); + *offset += 1; + self.gateway_fee_recipient = Some(rlp.val_at(*offset)?); + *offset += 1; + self.gateway_fee = Some(rlp.val_at(*offset)?); + *offset += 1; + Ok(()) + } + + /// Decodes fields of the type 2 transaction response starting at the RLP offset passed. + /// Increments the offset for each element parsed. + #[inline] + fn decode_base_eip1559( + &mut self, + rlp: &rlp::Rlp, + offset: &mut usize, + ) -> Result<(), DecoderError> { + self.chain_id = Some(rlp.val_at(*offset)?); + *offset += 1; + self.nonce = rlp.val_at(*offset)?; + *offset += 1; + self.max_priority_fee_per_gas = Some(rlp.val_at(*offset)?); + *offset += 1; + self.max_fee_per_gas = Some(rlp.val_at(*offset)?); + *offset += 1; + self.gas = rlp.val_at(*offset)?; + *offset += 1; + self.to = Some(rlp.val_at(*offset)?); + *offset += 1; + self.value = rlp.val_at(*offset)?; + *offset += 1; + let input = rlp::Rlp::new(rlp.at(*offset)?.as_raw()).data()?; + self.input = Bytes::from(input.to_vec()); + *offset += 1; + self.access_list = Some(rlp.val_at(*offset)?); + *offset += 1; + Ok(()) + } + + /// Decodes fields of the type 1 transaction response based on the RLP offset passed. + /// Increments the offset for each element parsed. + fn decode_base_eip2930( + &mut self, + rlp: &rlp::Rlp, + offset: &mut usize, + ) -> Result<(), DecoderError> { + self.chain_id = Some(rlp.val_at(*offset)?); + *offset += 1; + self.nonce = rlp.val_at(*offset)?; + *offset += 1; + self.gas_price = Some(rlp.val_at(*offset)?); + *offset += 1; + self.gas = rlp.val_at(*offset)?; + *offset += 1; + + #[cfg(feature = "celo")] + self.decode_celo_metadata(rlp, offset)?; + + self.gas = rlp.val_at(*offset)?; + *offset += 1; + self.to = Some(rlp.val_at(*offset)?); + *offset += 1; + self.value = rlp.val_at(*offset)?; + *offset += 1; + let input = rlp::Rlp::new(rlp.at(*offset)?.as_raw()).data()?; + self.input = Bytes::from(input.to_vec()); + *offset += 1; + self.access_list = Some(rlp.val_at(*offset)?); + *offset += 1; + + Ok(()) + } + + /// Decodes a legacy transaction starting at the RLP offset passed. + /// Increments the offset for each element parsed. + #[inline] + fn decode_base_legacy( + &mut self, + rlp: &rlp::Rlp, + offset: &mut usize, + ) -> Result<(), DecoderError> { + println!("are we a list {}", rlp.is_list()); + self.nonce = rlp.val_at(*offset)?; + *offset += 1; + self.gas_price = Some(rlp.val_at(*offset)?); + *offset += 1; + self.gas = rlp.val_at(*offset)?; + *offset += 1; + + #[cfg(feature = "celo")] + self.decode_celo_metadata(rlp, offset)?; + + self.to = Some(rlp.val_at(*offset)?); + *offset += 1; + self.value = rlp.val_at(*offset)?; + *offset += 1; + let input = rlp::Rlp::new(rlp.at(*offset)?.as_raw()).data()?; + self.input = Bytes::from(input.to_vec()); + *offset += 1; + Ok(()) + } +} + +/// Get a TransactionReceipt directly from an rlp encoded byte stream +impl Decodable for Transaction { + fn decode(rlp: &rlp::Rlp) -> Result { + let mut txn = Self::default(); + // we can get the type from the first value + let mut offset = 0; + txn.transaction_type = Some(rlp.data().unwrap().into()); + let rest = rlp::Rlp::new( + rlp.as_raw().get(1..).ok_or(DecoderError::Custom("no transaction payload"))?, + ); + + match txn.transaction_type { + Some(x) if x == U64::from(1) => { + // EIP-2930 (0x01) + txn.decode_base_eip2930(&rest, &mut offset)?; + } + Some(x) if x == U64::from(2) => { + // EIP-1559 (0x02) + txn.decode_base_eip1559(&rest, &mut offset)?; + } + _ => { + // Legacy (0x00) + txn.decode_base_legacy(&rest, &mut offset)?; + } + } + + let sig = decode_signature(&rest, &mut offset)?; + txn.r = sig.r; + txn.s = sig.s; + txn.v = sig.v.into(); + + Ok(txn) + } } /// "Receipt" of an executed transaction: details of its execution. @@ -401,4 +547,94 @@ mod tests { ) ); } + + #[test] + fn rlp_london_goerli() { + let tx = Transaction { + hash: H256::from_str( + "5e2fc091e15119c97722e9b63d5d32b043d077d834f377b91f80d32872c78109", + ) + .unwrap(), + nonce: 65.into(), + block_hash: Some( + H256::from_str("f43869e67c02c57d1f9a07bb897b54bec1cfa1feb704d91a2ee087566de5df2c") + .unwrap(), + ), + block_number: Some(6203173.into()), + transaction_index: Some(10.into()), + from: Address::from_str("e66b278fa9fbb181522f6916ec2f6d66ab846e04").unwrap(), + to: Some(Address::from_str("11d7c2ab0d4aa26b7d8502f6a7ef6844908495c2").unwrap()), + value: 0.into(), + gas_price: Some(1500000007.into()), + gas: 106703.into(), + input: hex::decode("e5225381").unwrap().into(), + v: 1.into(), + r: U256::from_str_radix( + "12010114865104992543118914714169554862963471200433926679648874237672573604889", + 10, + ) + .unwrap(), + s: U256::from_str_radix( + "22830728216401371437656932733690354795366167672037272747970692473382669718804", + 10, + ) + .unwrap(), + transaction_type: Some(2.into()), + access_list: Some(AccessList::default()), + max_priority_fee_per_gas: Some(1500000000.into()), + max_fee_per_gas: Some(1500000009.into()), + chain_id: Some(5.into()), + }; + assert_eq!( + tx.rlp(), + Bytes::from( + hex::decode("02f86f05418459682f008459682f098301a0cf9411d7c2ab0d4aa26b7d8502f6a7ef6844908495c28084e5225381c001a01a8d7bef47f6155cbdf13d57107fc577fd52880fa2862b1a50d47641f8839419a03279bbf73fde76de83440d04b9d97f3809fec8617d3557ee40ac3e0edc391514").unwrap() + ) + ); + } + + #[test] + fn decode_rlp_london_goerli() { + let tx = Transaction { + hash: H256::from_str( + "5e2fc091e15119c97722e9b63d5d32b043d077d834f377b91f80d32872c78109", + ) + .unwrap(), + nonce: 65.into(), + block_hash: Some( + H256::from_str("f43869e67c02c57d1f9a07bb897b54bec1cfa1feb704d91a2ee087566de5df2c") + .unwrap(), + ), + block_number: Some(6203173.into()), + transaction_index: Some(10.into()), + from: Address::from_str("e66b278fa9fbb181522f6916ec2f6d66ab846e04").unwrap(), + to: Some(Address::from_str("11d7c2ab0d4aa26b7d8502f6a7ef6844908495c2").unwrap()), + value: 0.into(), + gas_price: Some(1500000007.into()), + gas: 106703.into(), + input: hex::decode("e5225381").unwrap().into(), + v: 1.into(), + r: U256::from_str_radix( + "12010114865104992543118914714169554862963471200433926679648874237672573604889", + 10, + ) + .unwrap(), + s: U256::from_str_radix( + "22830728216401371437656932733690354795366167672037272747970692473382669718804", + 10, + ) + .unwrap(), + transaction_type: Some(2.into()), + access_list: Some(AccessList::default()), + max_priority_fee_per_gas: Some(1500000000.into()), + max_fee_per_gas: Some(1500000009.into()), + chain_id: Some(5.into()), + }; + + let rlp_bytes = hex::decode("02f86f05418459682f008459682f098301a0cf9411d7c2ab0d4aa26b7d8502f6a7ef6844908495c28084e5225381c001a01a8d7bef47f6155cbdf13d57107fc577fd52880fa2862b1a50d47641f8839419a03279bbf73fde76de83440d04b9d97f3809fec8617d3557ee40ac3e0edc391514").unwrap(); + let decoded_transaction = Transaction::decode(&rlp::Rlp::new(&rlp_bytes)).unwrap(); + + // we compare hash because the hash depends on the rlp encoding + assert_eq!(decoded_transaction.hash(), tx.hash()); + } } diff --git a/ethers-middleware/src/signer.rs b/ethers-middleware/src/signer.rs index 7234e94f..542df8ca 100644 --- a/ethers-middleware/src/signer.rs +++ b/ethers-middleware/src/signer.rs @@ -94,6 +94,9 @@ pub enum SignerMiddlewareError { /// Thrown if a signature is requested from a different address #[error("specified from address is not signer")] WrongSigner, + /// Thrown if the signer's chain_id is different than the chain_id of the transaction + #[error("specified chain_id is different than the signer's chain_id")] + DifferentChainID, } // Helper functions for locally signing transactions @@ -113,11 +116,25 @@ where &self, tx: TypedTransaction, ) -> Result> { + // compare chain_id and use signer's chain_id if the tranasaction's chain_id is None, + // return an error if they are not consistent + let chain_id = self.signer.chain_id(); + match tx.chain_id() { + Some(id) if id.as_u64() != chain_id => { + return Err(SignerMiddlewareError::DifferentChainID) + } + None => { + let mut tx = tx.clone(); + tx.set_chain_id(chain_id); + } + _ => {} + } + let signature = self.signer.sign_transaction(&tx).await.map_err(SignerMiddlewareError::SignerError)?; // Return the raw rlp-encoded signed transaction - Ok(tx.rlp_signed(self.signer.chain_id(), &signature)) + Ok(tx.rlp_signed(&signature)) } /// Returns the client's address @@ -190,6 +207,12 @@ where }; tx.set_from(from); + // get the signer's chain_id if the transaction does not set it + let chain_id = self.signer.chain_id(); + if tx.chain_id().is_none() { + tx.set_chain_id(chain_id); + } + let nonce = maybe(tx.nonce().cloned(), self.get_transaction_count(from, block)).await?; tx.set_nonce(nonce); self.inner() @@ -266,6 +289,7 @@ mod tests { nonce: Some(0.into()), gas_price: Some(21_000_000_000u128.into()), data: None, + chain_id: None, } .into(); let chain_id = 1u64; diff --git a/ethers-middleware/tests/nonce_manager.rs b/ethers-middleware/tests/nonce_manager.rs index e61759b0..30b3fa03 100644 --- a/ethers-middleware/tests/nonce_manager.rs +++ b/ethers-middleware/tests/nonce_manager.rs @@ -36,7 +36,10 @@ async fn nonce_manager() { let mut tx_hashes = Vec::new(); for _ in 0..10 { let tx = provider - .send_transaction(Eip1559TransactionRequest::new().to(address).value(100u64), None) + .send_transaction( + Eip1559TransactionRequest::new().to(address).value(100u64).chain_id(chain_id), + None, + ) .await .unwrap(); tx_hashes.push(*tx); diff --git a/ethers-middleware/tests/signer.rs b/ethers-middleware/tests/signer.rs index 5d4cd67b..e5cf3570 100644 --- a/ethers-middleware/tests/signer.rs +++ b/ethers-middleware/tests/signer.rs @@ -39,7 +39,7 @@ async fn send_eth() { let provider = SignerMiddleware::new(provider, wallet); // craft the transaction - let tx = TransactionRequest::new().to(wallet2.address()).value(10000); + let tx = TransactionRequest::new().to(wallet2.address()).value(10000).chain_id(chain_id); let balance_before = provider.get_balance(provider.address(), None).await.unwrap(); diff --git a/ethers-providers/src/lib.rs b/ethers-providers/src/lib.rs index 696fa8c4..8d932317 100644 --- a/ethers-providers/src/lib.rs +++ b/ethers-providers/src/lib.rs @@ -168,6 +168,7 @@ pub trait Middleware: Sync + Send + Debug { /// 4. Estimate gas usage _with_ access lists /// 5. Enable access lists IFF they are cheaper /// 6. Poll and set legacy or 1559 gas prices + /// 7. Set the chain_id with the provider's, if not already set /// /// It does NOT set the nonce by default. /// It MAY override the gas amount set by the user, if access lists are @@ -223,7 +224,6 @@ pub trait Middleware: Sync + Send + Debug { } let gas_price = original.gas_price().expect("filled"); - let chain_id = self.get_chainid().await?.low_u64(); let sign_futs: Vec<_> = (0..escalations) .map(|i| { let new_price = policy(gas_price, i); @@ -234,7 +234,7 @@ pub trait Middleware: Sync + Send + Debug { .map(|req| async move { self.sign_transaction(&req, self.default_sender().unwrap_or_default()) .await - .map(|sig| req.rlp_signed(chain_id, &sig)) + .map(|sig| req.rlp_signed(&sig)) }) .collect(); diff --git a/ethers-signers/src/aws/mod.rs b/ethers-signers/src/aws/mod.rs index 6009dfc3..798cb2db 100644 --- a/ethers-signers/src/aws/mod.rs +++ b/ethers-signers/src/aws/mod.rs @@ -235,7 +235,7 @@ impl<'a> super::Signer for AwsSigner<'a> { #[instrument(err)] async fn sign_transaction(&self, tx: &TypedTransaction) -> Result { - let sighash = tx.sighash(self.chain_id); + let sighash = tx.sighash(); self.sign_digest_with_eip155(sighash).await } diff --git a/ethers-signers/src/ledger/app.rs b/ethers-signers/src/ledger/app.rs index 558d0995..346231fc 100644 --- a/ethers-signers/src/ledger/app.rs +++ b/ethers-signers/src/ledger/app.rs @@ -118,7 +118,7 @@ impl LedgerEthereum { /// Signs an Ethereum transaction (requires confirmation on the ledger) pub async fn sign_tx(&self, tx: &TypedTransaction) -> Result { let mut payload = Self::path_to_bytes(&self.derivation); - payload.extend_from_slice(tx.rlp(self.chain_id).as_ref()); + payload.extend_from_slice(tx.rlp().as_ref()); self.sign_payload(INS::SIGN, payload).await } diff --git a/ethers-signers/src/wallet/mod.rs b/ethers-signers/src/wallet/mod.rs index 6e8dcf10..65e8e7d1 100644 --- a/ethers-signers/src/wallet/mod.rs +++ b/ethers-signers/src/wallet/mod.rs @@ -18,7 +18,7 @@ use ethers_core::{ }, types::{ transaction::{eip2718::TypedTransaction, eip712::Eip712}, - Address, Signature, H256, U256, + Address, Signature, H256, U256, U64, }, utils::hash_message, }; @@ -83,7 +83,23 @@ impl> Signer fo } async fn sign_transaction(&self, tx: &TypedTransaction) -> Result { - Ok(self.sign_transaction_sync(tx)) + let chain_id = tx.chain_id(); + match chain_id { + Some(id) => { + if U64::from(self.chain_id) != id { + return Err(WalletError::InvalidTransactionError( + "transaction chain_id does not match the signer".to_string(), + )) + } + Ok(self.sign_transaction_sync(tx)) + } + None => { + // in the case we don't have a chain_id, let's use the signer chain id instead + let mut tx_with_chain = tx.clone(); + tx_with_chain.set_chain_id(self.chain_id); + Ok(self.sign_transaction_sync(&tx_with_chain)) + } + } } async fn sign_typed_data( @@ -119,7 +135,7 @@ impl> Signer fo impl> Wallet { /// Synchronously signs the provided transaction. pub fn sign_transaction_sync(&self, tx: &TypedTransaction) -> Signature { - let sighash = tx.sighash(self.chain_id); + let sighash = tx.sighash(); self.sign_hash(sighash, true) } diff --git a/ethers-signers/src/wallet/private_key.rs b/ethers-signers/src/wallet/private_key.rs index a8b2e936..4998fd9c 100644 --- a/ethers-signers/src/wallet/private_key.rs +++ b/ethers-signers/src/wallet/private_key.rs @@ -46,6 +46,8 @@ pub enum WalletError { /// Error type from Eip712Error message #[error("error encoding eip712 struct: {0:?}")] Eip712Error(String), + #[error("invalid transaction: {0:?}")] + InvalidTransactionError(String), } impl Clone for Wallet { @@ -142,8 +144,8 @@ impl FromStr for Wallet { #[cfg(not(target_arch = "wasm32"))] mod tests { use super::*; - use crate::Signer; - use ethers_core::types::Address; + use crate::{Signer, TypedTransaction}; + use ethers_core::types::{Address, U64}; use tempfile::tempdir; #[tokio::test] @@ -195,7 +197,7 @@ mod tests { use ethers_core::types::TransactionRequest; // retrieved test vector from: // https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction - let tx = TransactionRequest { + let tx: TypedTransaction = TransactionRequest { from: None, to: Some("F0109fC8DF283027b6285cc889F5aA624EaC1F55".parse::
().unwrap().into()), value: Some(1_000_000_000.into()), @@ -203,16 +205,15 @@ mod tests { nonce: Some(0.into()), gas_price: Some(21_000_000_000u128.into()), data: None, + chain_id: Some(U64::one()), } .into(); - let chain_id = 1u64; - let wallet: Wallet = "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318".parse().unwrap(); - let wallet = wallet.with_chain_id(chain_id); + let wallet = wallet.with_chain_id(tx.chain_id().unwrap().as_u64()); let sig = wallet.sign_transaction(&tx).await.unwrap(); - let sighash = tx.sighash(chain_id); + let sighash = tx.sighash(); assert!(sig.verify(sighash, wallet.address).is_ok()); }