Implement RLP decoding for transactions (#805)

* Implement RLP decoding for transactions

* set chain_id in fill_transaction
This commit is contained in:
Dan Cline 2022-01-30 14:21:16 -05:00 committed by GitHub
parent 44499ae008
commit 01544ec4b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 889 additions and 61 deletions

View File

@ -1,5 +1,7 @@
use std::cmp::Ordering;
use crate::types::Address; use crate::types::Address;
use rlp::{Encodable, RlpStream}; use rlp::{Decodable, Encodable, RlpStream};
use serde::{ser::Error as SerializationError, Deserialize, Deserializer, Serialize, Serializer}; use serde::{ser::Error as SerializationError, Deserialize, Deserializer, Serialize, Serializer};
/// ENS name or Ethereum Address. Not RLP encoded/serialized if it's a name /// 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<Self, rlp::DecoderError> {
// 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 { impl From<&str> for NameOrAddress {
fn from(s: &str) -> Self { fn from(s: &str) -> Self {
NameOrAddress::Name(s.to_owned()) NameOrAddress::Name(s.to_owned())
@ -71,6 +92,8 @@ impl<'de> Deserialize<'de> for NameOrAddress {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use rlp::Rlp;
use super::*; use super::*;
#[test] #[test]
@ -103,6 +126,20 @@ mod tests {
assert_eq!(rlp.as_raw(), expected.as_raw()); 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] #[test]
fn serde_name_not_serialized() { fn serde_name_not_serialized() {
let name = NameOrAddress::Name("ens.eth".to_string()); let name = NameOrAddress::Name("ens.eth".to_string());

View File

@ -3,7 +3,7 @@ use crate::{
types::{Address, Bytes, NameOrAddress, Signature, H256, U256, U64}, types::{Address, Bytes, NameOrAddress, Signature, H256, U256, U64},
utils::keccak256, utils::keccak256,
}; };
use rlp::RlpStream; use rlp::{Decodable, DecoderError, RlpStream};
/// EIP-1559 transactions have 9 fields /// EIP-1559 transactions have 9 fields
const NUM_TX_FIELDS: usize = 9; const NUM_TX_FIELDS: usize = 9;
@ -57,6 +57,10 @@ pub struct Eip1559TransactionRequest {
/// baseFeePerGas and maxPriorityFeePerGas). The difference between maxFeePerGas and /// baseFeePerGas and maxPriorityFeePerGas). The difference between maxFeePerGas and
/// baseFeePerGas + maxPriorityFeePerGas is “refunded” to the user. /// baseFeePerGas + maxPriorityFeePerGas is “refunded” to the user.
pub max_fee_per_gas: Option<U256>, pub max_fee_per_gas: Option<U256>,
#[serde(rename = "chainId", default, skip_serializing_if = "Option::is_none")]
/// Chain ID (None for mainnet)
pub chain_id: Option<U64>,
} }
impl Eip1559TransactionRequest { impl Eip1559TransactionRequest {
@ -130,25 +134,34 @@ impl Eip1559TransactionRequest {
self self
} }
/// Sets the `chain_id` field in the transaction to the provided value
#[must_use]
pub fn chain_id<T: Into<U64>>(mut self, chain_id: T) -> Self {
self.chain_id = Some(chain_id.into());
self
}
/// Hashes the transaction's data with the provided chain id /// Hashes the transaction's data with the provided chain id
pub fn sighash<T: Into<U64>>(&self, chain_id: T) -> H256 { pub fn sighash(&self) -> H256 {
keccak256(self.rlp(chain_id).as_ref()).into() keccak256(self.rlp().as_ref()).into()
} }
/// Gets the unsigned transaction's RLP encoding /// Gets the unsigned transaction's RLP encoding
pub fn rlp<T: Into<U64>>(&self, chain_id: T) -> Bytes { pub fn rlp(&self) -> Bytes {
let mut rlp = RlpStream::new(); let mut rlp = RlpStream::new();
rlp.begin_list(NUM_TX_FIELDS); rlp.begin_list(NUM_TX_FIELDS);
self.rlp_base(chain_id, &mut rlp); self.rlp_base(&mut rlp);
rlp.out().freeze().into() rlp.out().freeze().into()
} }
/// Produces the RLP encoding of the transaction with the provided signature /// Produces the RLP encoding of the transaction with the provided signature
pub fn rlp_signed<T: Into<U64>>(&self, chain_id: T, signature: &Signature) -> Bytes { pub fn rlp_signed(&self, signature: &Signature) -> Bytes {
let mut rlp = RlpStream::new(); let mut rlp = RlpStream::new();
rlp.begin_unbounded_list(); rlp.begin_unbounded_list();
let chain_id = chain_id.into(); self.rlp_base(&mut rlp);
self.rlp_base(chain_id, &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 // append the signature
let v = normalize_v(signature.v, chain_id); let v = normalize_v(signature.v, chain_id);
@ -159,8 +172,8 @@ impl Eip1559TransactionRequest {
rlp.out().freeze().into() rlp.out().freeze().into()
} }
pub(crate) fn rlp_base<T: Into<U64>>(&self, chain_id: T, rlp: &mut RlpStream) { pub(crate) fn rlp_base(&self, rlp: &mut RlpStream) {
rlp.append(&chain_id.into()); rlp_opt(rlp, &self.chain_id);
rlp_opt(rlp, &self.nonce); rlp_opt(rlp, &self.nonce);
rlp_opt(rlp, &self.max_priority_fee_per_gas); rlp_opt(rlp, &self.max_priority_fee_per_gas);
rlp_opt(rlp, &self.max_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_opt(rlp, &self.data.as_ref().map(|d| d.as_ref()));
rlp.append(&self.access_list); 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<Self, rlp::DecoderError> {
let mut txn = Eip1559TransactionRequest::new();
let mut offset = 0;
txn.decode_base_rlp(rlp, &mut offset)?;
Ok(txn)
}
} }
impl From<Eip1559TransactionRequest> for super::request::TransactionRequest { impl From<Eip1559TransactionRequest> for super::request::TransactionRequest {
@ -188,6 +239,7 @@ impl From<Eip1559TransactionRequest> for super::request::TransactionRequest {
gateway_fee_recipient: None, gateway_fee_recipient: None,
#[cfg(feature = "celo")] #[cfg(feature = "celo")]
gateway_fee: None, gateway_fee: None,
chain_id: tx.chain_id,
} }
} }
} }

View File

@ -162,6 +162,23 @@ impl TypedTransaction {
}; };
} }
pub fn chain_id(&self) -> Option<U64> {
match self {
Legacy(inner) => inner.chain_id,
Eip2930(inner) => inner.tx.chain_id,
Eip1559(inner) => inner.chain_id,
}
}
pub fn set_chain_id<T: Into<U64>>(&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> { pub fn data(&self) -> Option<&Bytes> {
match self { match self {
Legacy(inner) => inner.data.as_ref(), Legacy(inner) => inner.data.as_ref(),
@ -194,7 +211,7 @@ impl TypedTransaction {
}; };
} }
pub fn rlp_signed<T: Into<U64>>(&self, chain_id: T, signature: &Signature) -> Bytes { pub fn rlp_signed(&self, signature: &Signature) -> Bytes {
let mut encoded = vec![]; let mut encoded = vec![];
match self { match self {
Legacy(ref tx) => { Legacy(ref tx) => {
@ -202,44 +219,71 @@ impl TypedTransaction {
} }
Eip2930(inner) => { Eip2930(inner) => {
encoded.extend_from_slice(&[0x1]); 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) => { Eip1559(inner) => {
encoded.extend_from_slice(&[0x2]); 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() encoded.into()
} }
pub fn rlp<T: Into<U64>>(&self, chain_id: T) -> Bytes { pub fn rlp(&self) -> Bytes {
let chain_id = chain_id.into();
let mut encoded = vec![]; let mut encoded = vec![];
match self { match self {
Legacy(inner) => { Legacy(inner) => {
encoded.extend_from_slice(inner.rlp(chain_id).as_ref()); encoded.extend_from_slice(inner.rlp().as_ref());
} }
Eip2930(inner) => { Eip2930(inner) => {
encoded.extend_from_slice(&[0x1]); 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) => { Eip1559(inner) => {
encoded.extend_from_slice(&[0x2]); 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() encoded.into()
} }
/// Hashes the transaction's data with the provided chain id /// Hashes the transaction's data. Does not double-RLP encode
/// Does not double-RLP encode pub fn sighash(&self) -> H256 {
pub fn sighash<T: Into<U64>>(&self, chain_id: T) -> H256 { let encoded = self.rlp();
let encoded = self.rlp(chain_id);
keccak256(encoded).into() keccak256(encoded).into()
} }
} }
/// Get a TypedTransaction directly from an rlp encoded byte stream
impl rlp::Decodable for TypedTransaction {
fn decode(rlp: &rlp::Rlp) -> Result<Self, rlp::DecoderError> {
let tx_type: Option<U64> = 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<TransactionRequest> for TypedTransaction { impl From<TransactionRequest> for TypedTransaction {
fn from(src: TransactionRequest) -> TypedTransaction { fn from(src: TransactionRequest) -> TypedTransaction {
TypedTransaction::Legacy(src) TypedTransaction::Legacy(src)
@ -260,8 +304,11 @@ impl From<Eip1559TransactionRequest> for TypedTransaction {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use rlp::Decodable;
use super::*; use super::*;
use crate::types::{Address, U256}; use crate::types::{Address, U256};
use std::str::FromStr;
#[test] #[test]
fn serde_legacy_tx() { fn serde_legacy_tx() {
@ -276,4 +323,102 @@ mod tests {
let de: TransactionRequest = serde_json::from_str(&serialized).unwrap(); let de: TransactionRequest = serde_json::from_str(&serialized).unwrap();
assert_eq!(tx, TypedTransaction::Legacy(de)); 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::<Address>().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());
}
} }

View File

@ -1,8 +1,8 @@
use super::{normalize_v, request::TransactionRequest}; use super::{normalize_v, request::TransactionRequest};
use crate::types::{Address, Bytes, Signature, H256, U256, U64}; use crate::types::{Address, Bytes, Signature, H256, U256, U64};
use rlp::RlpStream; use rlp::{Decodable, DecoderError, RlpStream};
use rlp_derive::{RlpEncodable, RlpEncodableWrapper}; use rlp_derive::{RlpDecodable, RlpDecodableWrapper, RlpEncodable, RlpEncodableWrapper};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
const NUM_EIP2930_FIELDS: usize = 8; const NUM_EIP2930_FIELDS: usize = 8;
@ -10,7 +10,17 @@ const NUM_EIP2930_FIELDS: usize = 8;
/// Access list /// Access list
// NB: Need to use `RlpEncodableWrapper` else we get an extra [] in the output // NB: Need to use `RlpEncodableWrapper` else we get an extra [] in the output
// https://github.com/gakonst/ethers-rs/pull/353#discussion_r680683869 // 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<AccessListItem>); pub struct AccessList(pub Vec<AccessListItem>);
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
@ -38,7 +48,9 @@ impl TransactionRequest {
} }
/// Access list item /// 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")] #[serde(rename_all = "camelCase")]
pub struct AccessListItem { pub struct AccessListItem {
/// Accessed address /// Accessed address
@ -60,10 +72,12 @@ impl Eip2930TransactionRequest {
Self { tx, access_list } Self { tx, access_list }
} }
pub fn rlp<T: Into<U64>>(&self, chain_id: T) -> Bytes { pub fn rlp(&self) -> Bytes {
let mut rlp = RlpStream::new(); let mut rlp = RlpStream::new();
rlp.begin_list(NUM_EIP2930_FIELDS); 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); self.tx.rlp_base(&mut rlp);
// append the access list in addition to the base rlp encoding // append the access list in addition to the base rlp encoding
rlp.append(&self.access_list); rlp.append(&self.access_list);
@ -72,11 +86,11 @@ impl Eip2930TransactionRequest {
} }
/// Produces the RLP encoding of the transaction with the provided signature /// Produces the RLP encoding of the transaction with the provided signature
pub fn rlp_signed<T: Into<U64>>(&self, chain_id: T, signature: &Signature) -> Bytes { pub fn rlp_signed(&self, signature: &Signature) -> Bytes {
let mut rlp = RlpStream::new(); let mut rlp = RlpStream::new();
rlp.begin_list(NUM_EIP2930_FIELDS + 3); 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); rlp.append(&chain_id);
self.tx.rlp_base(&mut rlp); self.tx.rlp_base(&mut rlp);
// append the access list in addition to the base rlp encoding // append the access list in addition to the base rlp encoding
@ -89,10 +103,50 @@ impl Eip2930TransactionRequest {
rlp.append(&signature.s); rlp.append(&signature.s);
rlp.out().freeze().into() 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<Self, rlp::DecoderError> {
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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::types::{transaction::eip2718::TypedTransaction, U256}; use crate::types::{transaction::eip2718::TypedTransaction, U256};
@ -110,14 +164,14 @@ mod tests {
.with_access_list(vec![]) .with_access_list(vec![])
.into(); .into();
let hash = tx.sighash(1); let hash = tx.sighash();
let sig: Signature = "c9519f4f2b30335884581971573fadf60c6204f59a911df35ee8a540456b266032f1e8e2c5dd761f9e4f88f41c8310aeaba26a8bfcdacfedfa12ec3862d3752101".parse().unwrap(); let sig: Signature = "c9519f4f2b30335884581971573fadf60c6204f59a911df35ee8a540456b266032f1e8e2c5dd761f9e4f88f41c8310aeaba26a8bfcdacfedfa12ec3862d3752101".parse().unwrap();
assert_eq!( assert_eq!(
hash, hash,
"49b486f0ec0a60dfbbca2d30cb07c9e8ffb2a2ff41f29a1ab6737475f6ff69f3".parse().unwrap() "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"; let expected = "b86601f8630103018261a894b94f5374fce5edbc8e2a8697c15331677e6ebf0b0a825544c001a0c9519f4f2b30335884581971573fadf60c6204f59a911df35ee8a540456b2660a032f1e8e2c5dd761f9e4f88f41c8310aeaba26a8bfcdacfedfa12ec3862d37521";
assert_eq!(hex::encode(&enc), expected); assert_eq!(hex::encode(&enc), expected);
} }

View File

@ -33,3 +33,44 @@ pub(crate) fn normalize_v(v: u64, chain_id: crate::types::U64) -> u64 {
v v
} }
} }
/// extracts the chainid from the signature v value based on EIP-155
pub(crate) fn extract_chain_id(v: u64) -> Option<crate::types::U64> {
// 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<super::Signature, rlp::DecoderError> {
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<U64> = None;
rlp_opt(&mut stream, &empty_chainid);
let out = stream.out();
assert_eq!(out, vec![0xc1, 0x80]);
}
}

View File

@ -1,11 +1,11 @@
//! Transaction types //! Transaction types
use super::{rlp_opt, NUM_TX_FIELDS}; use super::{extract_chain_id, rlp_opt, NUM_TX_FIELDS};
use crate::{ use crate::{
types::{Address, Bytes, NameOrAddress, Signature, H256, U256, U64}, types::{Address, Bytes, NameOrAddress, Signature, H256, U256, U64},
utils::keccak256, utils::keccak256,
}; };
use rlp::RlpStream; use rlp::{Decodable, RlpStream};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// Parameters for sending a transaction /// Parameters for sending a transaction
@ -41,6 +41,10 @@ pub struct TransactionRequest {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub nonce: Option<U256>, pub nonce: Option<U256>,
/// Chain ID (None for mainnet)
#[serde(skip_serializing_if = "Option::is_none")]
pub chain_id: Option<U64>,
///////////////// Celo-specific transaction fields ///////////////// ///////////////// Celo-specific transaction fields /////////////////
/// The currency fees are paid in (None for native currency) /// The currency fees are paid in (None for native currency)
#[cfg(feature = "celo")] #[cfg(feature = "celo")]
@ -123,29 +127,49 @@ impl TransactionRequest {
self self
} }
/// Hashes the transaction's data with the provided chain id /// Sets the `chain_id` field in the transaction to the provided value
pub fn sighash<T: Into<U64>>(&self, chain_id: T) -> H256 { #[must_use]
keccak256(self.rlp(chain_id).as_ref()).into() pub fn chain_id<T: Into<U64>>(mut self, chain_id: T) -> Self {
self.chain_id = Some(chain_id.into());
self
} }
/// Gets the unsigned transaction's RLP encoding /// Hashes the transaction's data with the provided chain id
pub fn rlp<T: Into<U64>>(&self, chain_id: T) -> Bytes { 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(); let mut rlp = RlpStream::new();
rlp.begin_list(NUM_TX_FIELDS); rlp.begin_list(NUM_TX_FIELDS);
self.rlp_base(&mut rlp); self.rlp_base(&mut rlp);
// Only hash the 3 extra fields when preparing the // Only hash the 3 extra fields when preparing the
// data to sign if chain_id is present // 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.append(&0u8); rlp.append(&0u8);
rlp.out().freeze().into() 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 /// Produces the RLP encoding of the transaction with the provided signature
pub fn rlp_signed(&self, signature: &Signature) -> Bytes { pub fn rlp_signed(&self, signature: &Signature) -> Bytes {
let mut rlp = RlpStream::new(); let mut rlp = RlpStream::new();
rlp.begin_list(NUM_TX_FIELDS); rlp.begin_list(NUM_TX_FIELDS);
self.rlp_base(&mut rlp); self.rlp_base(&mut rlp);
// append the signature // append the signature
@ -167,6 +191,86 @@ impl TransactionRequest {
rlp_opt(rlp, &self.value); rlp_opt(rlp, &self.value);
rlp_opt(rlp, &self.data.as_ref().map(|d| d.as_ref())); 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<Self, rlp::DecoderError> {
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<Self, rlp::DecoderError> {
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<Self, rlp::DecoderError> {
TransactionRequest::decode_unsigned_rlp(rlp)
}
} }
// Separate impl block for the celo-specific fields // Separate impl block for the celo-specific fields
@ -207,8 +311,46 @@ impl TransactionRequest {
#[cfg(test)] #[cfg(test)]
#[cfg(not(feature = "celo"))] #[cfg(not(feature = "celo"))]
mod tests { 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::<Address>().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::<Address>().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] #[test]
fn decode_unsigned_transaction() { fn decode_unsigned_transaction() {
let _res: TransactionRequest = serde_json::from_str( let _res: TransactionRequest = serde_json::from_str(
@ -226,4 +368,81 @@ mod tests {
) )
.unwrap(); .unwrap();
} }
#[test]
fn decode_known_rlp_goerli() {
let tx = TransactionRequest::new()
.nonce(70272)
.from("fab2b4b677a4e104759d378ea25504862150256e".parse::<Address>().unwrap())
.to("d1f23226fb4d2b7d2f3bcdd99381b038de705a64".parse::<Address>().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::<Address>().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::<Address>().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)));
}
} }

View File

@ -1,10 +1,10 @@
//! Transaction types //! Transaction types
use super::{eip2930::AccessList, normalize_v, rlp_opt}; use super::{decode_signature, eip2930::AccessList, normalize_v, rlp_opt};
use crate::{ use crate::{
types::{Address, Bloom, Bytes, Log, H256, U256, U64}, types::{Address, Bloom, Bytes, Log, H256, U256, U64},
utils::keccak256, utils::keccak256,
}; };
use rlp::RlpStream; use rlp::{Decodable, DecoderError, RlpStream};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// Details of a signed transaction /// Details of a signed transaction
@ -201,6 +201,152 @@ impl Transaction {
_ => rlp_bytes, _ => 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<Self, DecoderError> {
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. /// "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());
}
} }

View File

@ -94,6 +94,9 @@ pub enum SignerMiddlewareError<M: Middleware, S: Signer> {
/// Thrown if a signature is requested from a different address /// Thrown if a signature is requested from a different address
#[error("specified from address is not signer")] #[error("specified from address is not signer")]
WrongSigner, 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 // Helper functions for locally signing transactions
@ -113,11 +116,25 @@ where
&self, &self,
tx: TypedTransaction, tx: TypedTransaction,
) -> Result<Bytes, SignerMiddlewareError<M, S>> { ) -> Result<Bytes, SignerMiddlewareError<M, S>> {
// 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 = let signature =
self.signer.sign_transaction(&tx).await.map_err(SignerMiddlewareError::SignerError)?; self.signer.sign_transaction(&tx).await.map_err(SignerMiddlewareError::SignerError)?;
// Return the raw rlp-encoded signed transaction // 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 /// Returns the client's address
@ -190,6 +207,12 @@ where
}; };
tx.set_from(from); 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?; let nonce = maybe(tx.nonce().cloned(), self.get_transaction_count(from, block)).await?;
tx.set_nonce(nonce); tx.set_nonce(nonce);
self.inner() self.inner()
@ -266,6 +289,7 @@ mod tests {
nonce: Some(0.into()), nonce: Some(0.into()),
gas_price: Some(21_000_000_000u128.into()), gas_price: Some(21_000_000_000u128.into()),
data: None, data: None,
chain_id: None,
} }
.into(); .into();
let chain_id = 1u64; let chain_id = 1u64;

View File

@ -36,7 +36,10 @@ async fn nonce_manager() {
let mut tx_hashes = Vec::new(); let mut tx_hashes = Vec::new();
for _ in 0..10 { for _ in 0..10 {
let tx = provider 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 .await
.unwrap(); .unwrap();
tx_hashes.push(*tx); tx_hashes.push(*tx);

View File

@ -39,7 +39,7 @@ async fn send_eth() {
let provider = SignerMiddleware::new(provider, wallet); let provider = SignerMiddleware::new(provider, wallet);
// craft the transaction // 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(); let balance_before = provider.get_balance(provider.address(), None).await.unwrap();

View File

@ -168,6 +168,7 @@ pub trait Middleware: Sync + Send + Debug {
/// 4. Estimate gas usage _with_ access lists /// 4. Estimate gas usage _with_ access lists
/// 5. Enable access lists IFF they are cheaper /// 5. Enable access lists IFF they are cheaper
/// 6. Poll and set legacy or 1559 gas prices /// 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 does NOT set the nonce by default.
/// It MAY override the gas amount set by the user, if access lists are /// 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 gas_price = original.gas_price().expect("filled");
let chain_id = self.get_chainid().await?.low_u64();
let sign_futs: Vec<_> = (0..escalations) let sign_futs: Vec<_> = (0..escalations)
.map(|i| { .map(|i| {
let new_price = policy(gas_price, i); let new_price = policy(gas_price, i);
@ -234,7 +234,7 @@ pub trait Middleware: Sync + Send + Debug {
.map(|req| async move { .map(|req| async move {
self.sign_transaction(&req, self.default_sender().unwrap_or_default()) self.sign_transaction(&req, self.default_sender().unwrap_or_default())
.await .await
.map(|sig| req.rlp_signed(chain_id, &sig)) .map(|sig| req.rlp_signed(&sig))
}) })
.collect(); .collect();

View File

@ -235,7 +235,7 @@ impl<'a> super::Signer for AwsSigner<'a> {
#[instrument(err)] #[instrument(err)]
async fn sign_transaction(&self, tx: &TypedTransaction) -> Result<EthSig, Self::Error> { async fn sign_transaction(&self, tx: &TypedTransaction) -> Result<EthSig, Self::Error> {
let sighash = tx.sighash(self.chain_id); let sighash = tx.sighash();
self.sign_digest_with_eip155(sighash).await self.sign_digest_with_eip155(sighash).await
} }

View File

@ -118,7 +118,7 @@ impl LedgerEthereum {
/// Signs an Ethereum transaction (requires confirmation on the ledger) /// Signs an Ethereum transaction (requires confirmation on the ledger)
pub async fn sign_tx(&self, tx: &TypedTransaction) -> Result<Signature, LedgerError> { pub async fn sign_tx(&self, tx: &TypedTransaction) -> Result<Signature, LedgerError> {
let mut payload = Self::path_to_bytes(&self.derivation); 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 self.sign_payload(INS::SIGN, payload).await
} }

View File

@ -18,7 +18,7 @@ use ethers_core::{
}, },
types::{ types::{
transaction::{eip2718::TypedTransaction, eip712::Eip712}, transaction::{eip2718::TypedTransaction, eip712::Eip712},
Address, Signature, H256, U256, Address, Signature, H256, U256, U64,
}, },
utils::hash_message, utils::hash_message,
}; };
@ -83,7 +83,23 @@ impl<D: Sync + Send + DigestSigner<Sha256Proxy, RecoverableSignature>> Signer fo
} }
async fn sign_transaction(&self, tx: &TypedTransaction) -> Result<Signature, Self::Error> { async fn sign_transaction(&self, tx: &TypedTransaction) -> Result<Signature, Self::Error> {
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<T: Eip712 + Send + Sync>( async fn sign_typed_data<T: Eip712 + Send + Sync>(
@ -119,7 +135,7 @@ impl<D: Sync + Send + DigestSigner<Sha256Proxy, RecoverableSignature>> Signer fo
impl<D: DigestSigner<Sha256Proxy, RecoverableSignature>> Wallet<D> { impl<D: DigestSigner<Sha256Proxy, RecoverableSignature>> Wallet<D> {
/// Synchronously signs the provided transaction. /// Synchronously signs the provided transaction.
pub fn sign_transaction_sync(&self, tx: &TypedTransaction) -> Signature { 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) self.sign_hash(sighash, true)
} }

View File

@ -46,6 +46,8 @@ pub enum WalletError {
/// Error type from Eip712Error message /// Error type from Eip712Error message
#[error("error encoding eip712 struct: {0:?}")] #[error("error encoding eip712 struct: {0:?}")]
Eip712Error(String), Eip712Error(String),
#[error("invalid transaction: {0:?}")]
InvalidTransactionError(String),
} }
impl Clone for Wallet<SigningKey> { impl Clone for Wallet<SigningKey> {
@ -142,8 +144,8 @@ impl FromStr for Wallet<SigningKey> {
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
mod tests { mod tests {
use super::*; use super::*;
use crate::Signer; use crate::{Signer, TypedTransaction};
use ethers_core::types::Address; use ethers_core::types::{Address, U64};
use tempfile::tempdir; use tempfile::tempdir;
#[tokio::test] #[tokio::test]
@ -195,7 +197,7 @@ mod tests {
use ethers_core::types::TransactionRequest; use ethers_core::types::TransactionRequest;
// retrieved test vector from: // retrieved test vector from:
// https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction // https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction
let tx = TransactionRequest { let tx: TypedTransaction = TransactionRequest {
from: None, from: None,
to: Some("F0109fC8DF283027b6285cc889F5aA624EaC1F55".parse::<Address>().unwrap().into()), to: Some("F0109fC8DF283027b6285cc889F5aA624EaC1F55".parse::<Address>().unwrap().into()),
value: Some(1_000_000_000.into()), value: Some(1_000_000_000.into()),
@ -203,16 +205,15 @@ mod tests {
nonce: Some(0.into()), nonce: Some(0.into()),
gas_price: Some(21_000_000_000u128.into()), gas_price: Some(21_000_000_000u128.into()),
data: None, data: None,
chain_id: Some(U64::one()),
} }
.into(); .into();
let chain_id = 1u64;
let wallet: Wallet<SigningKey> = let wallet: Wallet<SigningKey> =
"4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318".parse().unwrap(); "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 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()); assert!(sig.verify(sighash, wallet.address).is_ok());
} }