From fd3df2938d2568850e84353a4eb4756d6f544de5 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 7 Jun 2024 08:28:27 -0400 Subject: [PATCH 1/7] feature: Eip658Value --- crates/consensus/src/lib.rs | 4 +- crates/consensus/src/receipt/any.rs | 28 ++- crates/consensus/src/receipt/envelope.rs | 8 +- crates/consensus/src/receipt/mod.rs | 34 +++- crates/consensus/src/receipt/receipts.rs | 50 ++++- crates/consensus/src/receipt/status.rs | 181 ++++++++++++++++++ .../rpc-types/src/eth/transaction/receipt.rs | 10 +- 7 files changed, 289 insertions(+), 26 deletions(-) create mode 100644 crates/consensus/src/receipt/status.rs diff --git a/crates/consensus/src/lib.rs b/crates/consensus/src/lib.rs index 75a68dda937..da2e1cb1dcc 100644 --- a/crates/consensus/src/lib.rs +++ b/crates/consensus/src/lib.rs @@ -19,7 +19,9 @@ mod header; pub use header::{Header, EMPTY_OMMER_ROOT_HASH, EMPTY_ROOT_HASH}; mod receipt; -pub use receipt::{AnyReceiptEnvelope, Receipt, ReceiptEnvelope, ReceiptWithBloom, TxReceipt}; +pub use receipt::{ + AnyReceiptEnvelope, Eip658Value, Receipt, ReceiptEnvelope, ReceiptWithBloom, TxReceipt, +}; mod request; pub use request::Request; diff --git a/crates/consensus/src/receipt/any.rs b/crates/consensus/src/receipt/any.rs index 104dd12a8ba..ca285469fc1 100644 --- a/crates/consensus/src/receipt/any.rs +++ b/crates/consensus/src/receipt/any.rs @@ -1,4 +1,4 @@ -use crate::{ReceiptWithBloom, TxReceipt}; +use crate::{Eip658Value, ReceiptWithBloom, TxReceipt}; use alloy_eips::eip2718::{Decodable2718, Encodable2718}; use alloy_primitives::{bytes::BufMut, Bloom, Log}; use alloy_rlp::{Decodable, Encodable}; @@ -47,13 +47,27 @@ impl AnyReceiptEnvelope { } /// Return true if the transaction was successful. + /// + /// ## Note + /// + /// This method may not accurately reflect the status of the transaction + /// for transactions before [EIP-658]. + /// + /// [EIP-658]: https://eips.ethereum.org/EIPS/eip-658 pub const fn is_success(&self) -> bool { self.status() } /// Returns the success status of the receipt's transaction. + /// + /// ## Note + /// + /// This method may not accurately reflect the status of the transaction + /// for transactions before [EIP-658]. + /// + /// [EIP-658]: https://eips.ethereum.org/EIPS/eip-658 pub const fn status(&self) -> bool { - self.inner.receipt.status + matches!(self.inner.receipt.status, Eip658Value::Eip658(true) | Eip658Value::PostState(_)) } /// Return the receipt's bloom. @@ -73,22 +87,22 @@ impl AnyReceiptEnvelope { } impl TxReceipt for AnyReceiptEnvelope { - /// Returns the success status of the receipt's transaction. + fn status_or_post_state(&self) -> &Eip658Value { + self.inner.status_or_post_state() + } + fn status(&self) -> bool { - self.inner.receipt.status + self.inner.status() } - /// Return the receipt's bloom. fn bloom(&self) -> Bloom { self.inner.logs_bloom } - /// Returns the cumulative gas used at this receipt. fn cumulative_gas_used(&self) -> u128 { self.inner.receipt.cumulative_gas_used } - /// Return the receipt logs. fn logs(&self) -> &[T] { &self.inner.receipt.logs } diff --git a/crates/consensus/src/receipt/envelope.rs b/crates/consensus/src/receipt/envelope.rs index a6e42e3e4c0..93d722ef880 100644 --- a/crates/consensus/src/receipt/envelope.rs +++ b/crates/consensus/src/receipt/envelope.rs @@ -58,7 +58,7 @@ impl ReceiptEnvelope { /// Returns the success status of the receipt's transaction. pub fn status(&self) -> bool { - self.as_receipt().unwrap().status + self.as_receipt().unwrap().status.coerce_status() } /// Returns the cumulative gas used at this receipt. @@ -96,8 +96,12 @@ impl ReceiptEnvelope { } impl TxReceipt for ReceiptEnvelope { + fn status_or_post_state(&self) -> &crate::Eip658Value { + &self.as_receipt().unwrap().status + } + fn status(&self) -> bool { - self.as_receipt().unwrap().status + self.as_receipt().unwrap().status.coerce_status() } /// Return the receipt's bloom. diff --git a/crates/consensus/src/receipt/mod.rs b/crates/consensus/src/receipt/mod.rs index e0fbc2e4d88..b9f172baa06 100644 --- a/crates/consensus/src/receipt/mod.rs +++ b/crates/consensus/src/receipt/mod.rs @@ -9,10 +9,36 @@ pub use envelope::ReceiptEnvelope; mod receipts; pub use receipts::{Receipt, ReceiptWithBloom}; +mod status; +pub use status::Eip658Value; + /// Receipt is the result of a transaction execution. #[doc(alias = "TransactionReceipt")] pub trait TxReceipt { - /// Returns true if the transaction was successful. + /// Returns the status or post state of the transaction. + /// + /// ## Note + /// + /// Use this method instead of [`TxReceipt::status`] when the transaction + /// is pre-[EIP-658]. + /// + /// [EIP-658]: https://eips.ethereum.org/EIPS/eip-658 + fn status_or_post_state(&self) -> &Eip658Value; + + /// Returns true if the transaction was successful OR if the transaction is + /// pre-[EIP-658]. Results for transactions before [EIP-658] are not + /// reliable. + /// + /// ## Note + /// + /// Caution must be taken when using this method for deep-historical + /// receipts, as it may not accurately reflect the status of the + /// transaction. The transaction status is not knowable from the receipt + /// for transactions before [EIP-658]. + /// + /// This can be handled using [`TxReceipt::status_or_post_state`]. + /// + /// [EIP-658]: https://eips.ethereum.org/EIPS/eip-658 fn status(&self) -> bool; /// Returns the bloom filter for the logs in the receipt. This operation @@ -59,7 +85,7 @@ mod tests { bytes!("0100ff"), ), }], - status: false, + status: false.into(), }, logs_bloom: [0; 256].into(), }); @@ -91,7 +117,7 @@ mod tests { bytes!("0100ff"), ), }], - status: false, + status: false.into(), }, logs_bloom: [0; 256].into(), }; @@ -104,7 +130,7 @@ mod tests { fn gigantic_receipt() { let receipt = Receipt { cumulative_gas_used: 16747627, - status: true, + status: true.into(), logs: vec![ Log { address: address!("4bf56695415f725e43c3e04354b604bcfb6dfb6e"), diff --git a/crates/consensus/src/receipt/receipts.rs b/crates/consensus/src/receipt/receipts.rs index 69341d3a810..d1e8d7d7da3 100644 --- a/crates/consensus/src/receipt/receipts.rs +++ b/crates/consensus/src/receipt/receipts.rs @@ -1,15 +1,14 @@ -use core::borrow::Borrow; - -use super::TxReceipt; -use alloy_primitives::{Bloom, Log}; +use crate::receipt::{Eip658Value, TxReceipt}; +use alloy_primitives::{Bloom, Log, U128}; use alloy_rlp::{length_of_length, BufMut, Decodable, Encodable}; +use core::borrow::Borrow; #[cfg(not(feature = "std"))] use alloc::vec::Vec; /// Receipt containing result of transaction execution. #[derive(Clone, Debug, Default, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(serde::Deserialize))] #[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))] #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] #[doc(alias = "TransactionReceipt", alias = "TxReceipt")] @@ -17,8 +16,8 @@ pub struct Receipt { /// If transaction is executed successfully. /// /// This is the `statusCode` - #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity_bool"))] - pub status: bool, + #[cfg_attr(feature = "serde", serde(alias = "name"))] + pub status: Eip658Value, /// Gas used #[cfg_attr(feature = "serde", serde(with = "alloy_serde::u128_via_ruint"))] pub cumulative_gas_used: u128, @@ -26,6 +25,31 @@ pub struct Receipt { pub logs: Vec, } +#[cfg(feature = "serde")] +impl serde::Serialize for Receipt +where + T: serde::Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + + let mut s = serializer.serialize_struct("Receipt", 3)?; + + // If the status is EIP-658, serialize the status field. + // Otherwise, serialize the root field. + let key = if self.status.is_eip658() { "status" } else { "root" }; + s.serialize_field(key, &self.status)?; + + s.serialize_field("cumulativeGasUsed", &U128::from(self.cumulative_gas_used))?; + s.serialize_field("logs", &self.logs)?; + + s.end() + } +} + impl Receipt where T: Borrow, @@ -47,8 +71,12 @@ impl TxReceipt for Receipt where T: Borrow, { + fn status_or_post_state(&self) -> &Eip658Value { + &self.status + } + fn status(&self) -> bool { - self.status + self.status.coerce_status() } fn bloom(&self) -> Bloom { @@ -90,8 +118,12 @@ pub struct ReceiptWithBloom { } impl TxReceipt for ReceiptWithBloom { + fn status_or_post_state(&self) -> &Eip658Value { + &self.receipt.status + } + fn status(&self) -> bool { - self.receipt.status + matches!(self.receipt.status, Eip658Value::Eip658(true) | Eip658Value::PostState(_)) } fn bloom(&self) -> Bloom { diff --git a/crates/consensus/src/receipt/status.rs b/crates/consensus/src/receipt/status.rs new file mode 100644 index 00000000000..1f9893527e3 --- /dev/null +++ b/crates/consensus/src/receipt/status.rs @@ -0,0 +1,181 @@ +use alloy_primitives::B256; +use alloy_rlp::{BufMut, Decodable, Encodable, Error, Header}; + +#[cfg(not(feature = "std"))] +use alloc::vec::Vec; +/// Captures the result of a transaction execution. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))] +pub enum Eip658Value { + /// A boolean `statusCode` introduced by [EIP-658]. + /// + /// [EIP-658]: https://eips.ethereum.org/EIPS/eip-658 + Eip658(bool), + /// A pre-[EIP-658] hash value. + /// + /// [EIP-658]: https://eips.ethereum.org/EIPS/eip-658 + PostState(B256), +} + +impl Eip658Value { + /// Returns true if the transaction was successful OR if the transaction + /// is pre-[EIP-658]. + /// + /// [EIP-658]: https://eips.ethereum.org/EIPS/eip-658 + pub const fn coerce_status(&self) -> bool { + matches!(self, Self::Eip658(true) | Self::PostState(_)) + } + + /// Returns true if the transaction was a pre-[EIP-658] transaction. + /// + /// [EIP-658]: https://eips.ethereum.org/EIPS/eip-658 + pub const fn is_post_state(&self) -> bool { + matches!(self, Self::PostState(_)) + } + + /// Returns true if the transaction was a post-[EIP-658] transaction. + pub const fn is_eip658(&self) -> bool { + matches!(self, Self::PostState(_)) + } + + /// Fallibly convert to the post state. + pub const fn as_post_state(&self) -> Option { + match self { + Self::PostState(state) => Some(*state), + _ => None, + } + } + + /// Fallibly convert to the [EIP-658] status code. + /// + /// [EIP-658]: https://eips.ethereum.org/EIPS/eip-658 + pub const fn as_eip658(&self) -> Option { + match self { + Self::Eip658(status) => Some(*status), + _ => None, + } + } +} + +impl From for Eip658Value { + fn from(status: bool) -> Self { + Self::Eip658(status) + } +} + +impl From for Eip658Value { + fn from(state: B256) -> Self { + Self::PostState(state) + } +} + +// NB: default to success +impl Default for Eip658Value { + fn default() -> Self { + Self::Eip658(true) + } +} + +#[cfg(feature = "serde")] +impl serde::Serialize for Eip658Value { + fn serialize(&self, serializer: S) -> Result { + match self { + Self::Eip658(status) => { + if serializer.is_human_readable() { + serializer.serialize_str(if *status { "0x1" } else { "0x" }) + } else { + serializer.serialize_bool(*status) + } + } + Self::PostState(state) => state.serialize(serializer), + } + } +} + +#[cfg(feature = "serde")] +// NB: some visit methods partially or wholly copied from alloy-primitives +impl<'de> serde::Deserialize<'de> for Eip658Value { + fn deserialize>(deserializer: D) -> Result { + use serde::de; + struct Visitor; + + impl<'de> de::Visitor<'de> for Visitor { + type Value = Eip658Value; + + fn expecting(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter.write_str("a boolean or a 32-byte hash") + } + + fn visit_bool(self, v: bool) -> Result { + Ok(Eip658Value::Eip658(v)) + } + + fn visit_bytes(self, v: &[u8]) -> Result { + B256::try_from(v).map(Eip658Value::PostState).map_err(de::Error::custom) + } + + fn visit_seq>(self, mut seq: A) -> Result { + let len_error = |i| de::Error::invalid_length(i, &"exactly 32 bytes"); + let mut bytes = [0u8; 32]; + + for (i, byte) in bytes.iter_mut().enumerate() { + *byte = seq.next_element()?.ok_or_else(|| len_error(i))?; + } + + if let Ok(Some(_)) = seq.next_element::() { + return Err(len_error(33)); + } + + Ok(Eip658Value::PostState(bytes.into())) + } + + fn visit_str(self, v: &str) -> Result { + match v { + "0x" | "0x0" | "false" => Ok(Eip658Value::Eip658(false)), + "0x1" | "true" => Ok(Eip658Value::Eip658(true)), + _ => v.parse::().map(Eip658Value::PostState).map_err(de::Error::custom), + } + } + } + + deserializer.deserialize_any(Visitor) + } +} + +impl Encodable for Eip658Value { + fn encode(&self, buf: &mut dyn BufMut) { + match self { + Self::Eip658(status) => { + status.encode(buf); + } + Self::PostState(state) => { + state.encode(buf); + } + } + } + + fn length(&self) -> usize { + match self { + Self::Eip658(_) => 1, + Self::PostState(_) => 32, + } + } +} + +impl Decodable for Eip658Value { + fn decode(buf: &mut &[u8]) -> Result { + let h = Header::decode(buf)?; + + match h.payload_length { + 1 => { + let status = bool::decode(buf)?; + Ok(Self::Eip658(status)) + } + 32 => { + let state = B256::decode(buf)?; + Ok(Self::PostState(state)) + } + _ => Err(Error::UnexpectedLength), + } + } +} diff --git a/crates/rpc-types/src/eth/transaction/receipt.rs b/crates/rpc-types/src/eth/transaction/receipt.rs index 00c98647c5a..0fb4d384104 100644 --- a/crates/rpc-types/src/eth/transaction/receipt.rs +++ b/crates/rpc-types/src/eth/transaction/receipt.rs @@ -83,7 +83,7 @@ impl TransactionReceipt { ReceiptEnvelope::Eip1559(receipt) | ReceiptEnvelope::Eip2930(receipt) | ReceiptEnvelope::Eip4844(receipt) - | ReceiptEnvelope::Legacy(receipt) => receipt.receipt.status, + | ReceiptEnvelope::Legacy(receipt) => receipt.receipt.status.coerce_status(), _ => false, } } @@ -137,7 +137,7 @@ pub type AnyTransactionReceipt = WithOtherFields Date: Fri, 7 Jun 2024 08:59:51 -0400 Subject: [PATCH 2/7] feat: support pre-658 receipts --- crates/consensus/src/receipt/status.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/consensus/src/receipt/status.rs b/crates/consensus/src/receipt/status.rs index 1f9893527e3..e12d9a6eb9c 100644 --- a/crates/consensus/src/receipt/status.rs +++ b/crates/consensus/src/receipt/status.rs @@ -1,5 +1,5 @@ use alloy_primitives::B256; -use alloy_rlp::{BufMut, Decodable, Encodable, Error, Header}; +use alloy_rlp::{Buf, BufMut, Decodable, Encodable, Error, Header}; #[cfg(not(feature = "std"))] use alloc::vec::Vec; @@ -35,7 +35,7 @@ impl Eip658Value { /// Returns true if the transaction was a post-[EIP-658] transaction. pub const fn is_eip658(&self) -> bool { - matches!(self, Self::PostState(_)) + !matches!(self, Self::PostState(_)) } /// Fallibly convert to the post state. @@ -167,8 +167,9 @@ impl Decodable for Eip658Value { let h = Header::decode(buf)?; match h.payload_length { + 0 => Ok(Self::Eip658(false)), 1 => { - let status = bool::decode(buf)?; + let status = buf.get_u8() != 0; Ok(Self::Eip658(status)) } 32 => { From a3f0efd1f7951e3017c1f5a8b3ce2c37a2410000 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 7 Jun 2024 09:01:38 -0400 Subject: [PATCH 3/7] chore: match previous behavior more closely --- crates/consensus/src/receipt/status.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/crates/consensus/src/receipt/status.rs b/crates/consensus/src/receipt/status.rs index e12d9a6eb9c..5ec5574c8e5 100644 --- a/crates/consensus/src/receipt/status.rs +++ b/crates/consensus/src/receipt/status.rs @@ -80,13 +80,7 @@ impl Default for Eip658Value { impl serde::Serialize for Eip658Value { fn serialize(&self, serializer: S) -> Result { match self { - Self::Eip658(status) => { - if serializer.is_human_readable() { - serializer.serialize_str(if *status { "0x1" } else { "0x" }) - } else { - serializer.serialize_bool(*status) - } - } + Self::Eip658(status) => alloy_serde::quantity_bool::serialize(status, serializer), Self::PostState(state) => state.serialize(serializer), } } From 7e6f6be9078df84b2e4741e134f532dfedf4fc6d Mon Sep 17 00:00:00 2001 From: James Date: Fri, 7 Jun 2024 09:21:19 -0400 Subject: [PATCH 4/7] fix: incorrect decode impl --- crates/consensus/src/receipt/status.rs | 45 ++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/crates/consensus/src/receipt/status.rs b/crates/consensus/src/receipt/status.rs index 5ec5574c8e5..14598a0fa4b 100644 --- a/crates/consensus/src/receipt/status.rs +++ b/crates/consensus/src/receipt/status.rs @@ -164,13 +164,52 @@ impl Decodable for Eip658Value { 0 => Ok(Self::Eip658(false)), 1 => { let status = buf.get_u8() != 0; - Ok(Self::Eip658(status)) + Ok(status.into()) } 32 => { - let state = B256::decode(buf)?; - Ok(Self::PostState(state)) + if buf.remaining() < 32 { + return Err(Error::InputTooShort); + } + let mut state = B256::default(); + buf.copy_to_slice(state.as_mut_slice()); + Ok(state.into()) } _ => Err(Error::UnexpectedLength), } } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn rlp_sanity() { + let mut buf = Vec::new(); + let status = Eip658Value::Eip658(true); + status.encode(&mut buf); + assert_eq!(Eip658Value::decode(&mut buf.as_slice()), Ok(status)); + + let mut buf = Vec::new(); + let state = Eip658Value::PostState(B256::default()); + state.encode(&mut buf); + assert_eq!(Eip658Value::decode(&mut buf.as_slice()), Ok(state)); + } + + #[cfg(feature = "serde")] + #[test] + fn serde_sanity() { + let status: Eip658Value = true.into(); + let json = serde_json::to_string(&status).unwrap(); + assert_eq!(json, r#""0x1""#); + assert_eq!(serde_json::from_str::(&json).unwrap(), status); + + let state: Eip658Value = false.into(); + let json = serde_json::to_string(&state).unwrap(); + assert_eq!(json, r#""0x0""#); + + let state: Eip658Value = B256::repeat_byte(1).into(); + let json = serde_json::to_string(&state).unwrap(); + assert_eq!(json, r#""0x0101010101010101010101010101010101010101010101010101010101010101""#); + } +} From b6c4f384ccff700b20a8b4b3de84e65a331ef794 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 7 Jun 2024 09:21:47 -0400 Subject: [PATCH 5/7] lint: fmt --- crates/rpc-types/src/eth/transaction/receipt.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rpc-types/src/eth/transaction/receipt.rs b/crates/rpc-types/src/eth/transaction/receipt.rs index 0fb4d384104..996b6c099cd 100644 --- a/crates/rpc-types/src/eth/transaction/receipt.rs +++ b/crates/rpc-types/src/eth/transaction/receipt.rs @@ -137,7 +137,7 @@ pub type AnyTransactionReceipt = WithOtherFields Date: Fri, 7 Jun 2024 13:14:14 -0400 Subject: [PATCH 6/7] fix: root --- crates/consensus/src/receipt/receipts.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/consensus/src/receipt/receipts.rs b/crates/consensus/src/receipt/receipts.rs index d1e8d7d7da3..403bb93a168 100644 --- a/crates/consensus/src/receipt/receipts.rs +++ b/crates/consensus/src/receipt/receipts.rs @@ -16,7 +16,7 @@ pub struct Receipt { /// If transaction is executed successfully. /// /// This is the `statusCode` - #[cfg_attr(feature = "serde", serde(alias = "name"))] + #[cfg_attr(feature = "serde", serde(alias = "root"))] pub status: Eip658Value, /// Gas used #[cfg_attr(feature = "serde", serde(with = "alloy_serde::u128_via_ruint"))] From 7b18358f37997ed8114836a456cae5170ef71d34 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 7 Jun 2024 14:29:26 -0400 Subject: [PATCH 7/7] test: add one for root/status --- crates/consensus/src/receipt/receipts.rs | 28 ++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/crates/consensus/src/receipt/receipts.rs b/crates/consensus/src/receipt/receipts.rs index 403bb93a168..de4a6cd37f4 100644 --- a/crates/consensus/src/receipt/receipts.rs +++ b/crates/consensus/src/receipt/receipts.rs @@ -248,3 +248,31 @@ where Ok(Self { receipt: Receipt::::arbitrary(u)?, logs_bloom: Bloom::arbitrary(u)? }) } } + +#[cfg(test)] +mod test { + #[cfg(feature = "serde")] + #[test] + fn root_vs_status() { + let receipt = super::Receipt::<()> { + status: super::Eip658Value::Eip658(true), + cumulative_gas_used: 0, + logs: Vec::new(), + }; + + let json = serde_json::to_string(&receipt).unwrap(); + assert_eq!(json, r#"{"status":"0x1","cumulativeGasUsed":"0x0","logs":[]}"#); + + let receipt = super::Receipt::<()> { + status: super::Eip658Value::PostState(Default::default()), + cumulative_gas_used: 0, + logs: Vec::new(), + }; + + let json = serde_json::to_string(&receipt).unwrap(); + assert_eq!( + json, + r#"{"root":"0x0000000000000000000000000000000000000000000000000000000000000000","cumulativeGasUsed":"0x0","logs":[]}"# + ); + } +}