Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ Morph Reth is the next-generation execution client for [Morph](https://www.morph

### Key Features

- **L1 Message Support**: Native handling of L1-to-L2 deposit messages with queue index validation
- **Alt Fee Tokens**: Support for paying transaction fees in alternative ERC-20 tokens
- **Custom Receipt Types**: Extended receipt format with L1 fee tracking
- **L1 Message Support**: Seamless bridging of assets and messages from Ethereum L1 to Morph L2
- **Morph Transaction**: Morph EVM+ transaction enabling alternative token fees, reference key indexing, and memo attachment
- **Morph Hardforks**: Full support for Morph's upgrade schedule (Bernoulli, Curie, Morph203, Viridian, Emerald)

## Architecture
Expand Down Expand Up @@ -91,7 +90,7 @@ Morph supports the following transaction types:
| EIP-1559 | `0x02` | Dynamic fee transactions |
| EIP-7702 | `0x04` | Account abstraction transactions |
| L1 Message | `0x7e` | L1-to-L2 deposit messages |
| Alt Fee | `0x7f` | Alternative fee token transactions |
| Morph Transaction | `0x7f` | Morph EVM+ transaction with enhanced features |

### L1 Messages

Expand All @@ -102,6 +101,16 @@ L1 messages are special deposit transactions that originate from Ethereum L1:
- Gas is prepaid on L1, so no L2 gas fee is charged
- Cannot be sent via the mempool (sequencer only)

### Morph Transaction

Morph Transaction (`0x7f`) is Morph's EVM+ transaction type, extending standard EVM transactions for better user experience and enterprise integration:

| Feature | Description |
|---------|-------------|
| **Alternative Fee Tokens** | Pay gas in stablecoins (USDT, USDC) or other ERC-20 tokens — no ETH required |
| **Transaction Reference** | Tag transactions with a 32-byte key for order tracking and payment reconciliation |
| **Memo Field** | Attach notes or invoice numbers (up to 64 bytes) for auditing and record-keeping |

### Hardforks

| Hardfork | Description |
Expand Down
50 changes: 34 additions & 16 deletions crates/primitives/src/receipt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -447,8 +447,8 @@ mod compact {
/// Note: `tx_type` must be the last field because it's not a known fixed-size type
/// for the CompactZstd derive macro.
///
/// Note: `fee_token_id` is stored as `u64` instead of `u16` because `u16` doesn't implement
/// `Compact` in reth_codecs. The conversion is lossless since `u16` fits in `u64`.
/// Note: `fee_token_id` and `version` are stored as `u64` instead of `u16`/`u8` because
/// they don't implement `Compact` in reth_codecs. The conversion is lossless.
#[derive(reth_codecs::CompactZstd)]
#[reth_zstd(
compressor = reth_zstd_compressors::RECEIPT_COMPRESSOR,
Expand All @@ -460,41 +460,53 @@ mod compact {
#[allow(clippy::owned_cow)]
logs: Cow<'a, Vec<Log>>,
l1_fee: Option<U256>,
/// Stored as u64 for Compact compatibility (u8 doesn't implement Compact)
version: Option<u64>,
/// Stored as u64 for Compact compatibility (u16 doesn't implement Compact)
fee_token_id: Option<u64>,
fee_rate: Option<U256>,
token_scale: Option<U256>,
fee_limit: Option<U256>,
reference: Option<B256>,
#[allow(clippy::owned_cow)]
memo: Option<Cow<'a, alloy_primitives::Bytes>>,
/// Must be the last field - not a known fixed-size type
tx_type: MorphTxType,
}

impl<'a> From<&'a MorphReceipt> for CompactMorphReceipt<'a> {
fn from(receipt: &'a MorphReceipt) -> Self {
let (l1_fee, fee_token_id, fee_rate, token_scale, fee_limit) = match receipt {
MorphReceipt::Legacy(r)
| MorphReceipt::Eip2930(r)
| MorphReceipt::Eip1559(r)
| MorphReceipt::Eip7702(r)
| MorphReceipt::Morph(r) => (
r.l1_fee,
r.fee_token_id.map(u64::from),
r.fee_rate,
r.token_scale,
r.fee_limit,
),
MorphReceipt::L1Msg(_) => (None, None, None, None, None),
};
let (l1_fee, version, fee_token_id, fee_rate, token_scale, fee_limit, reference, memo) =
match receipt {
MorphReceipt::Legacy(r)
| MorphReceipt::Eip2930(r)
| MorphReceipt::Eip1559(r)
| MorphReceipt::Eip7702(r)
| MorphReceipt::Morph(r) => (
r.l1_fee,
r.version.map(u64::from),
r.fee_token_id.map(u64::from),
r.fee_rate,
r.token_scale,
r.fee_limit,
r.reference,
r.memo.as_ref().map(Cow::Borrowed),
),
MorphReceipt::L1Msg(_) => (None, None, None, None, None, None, None, None),
};

Self {
success: receipt.status(),
cumulative_gas_used: receipt.cumulative_gas_used(),
logs: Cow::Borrowed(&receipt.as_receipt().logs),
l1_fee,
version,
fee_token_id,
fee_rate,
token_scale,
fee_limit,
reference,
memo,
tx_type: receipt.tx_type(),
}
}
Expand All @@ -507,10 +519,13 @@ mod compact {
cumulative_gas_used,
logs,
l1_fee,
version,
fee_token_id,
fee_rate,
token_scale,
fee_limit,
reference,
memo,
tx_type,
} = receipt;

Expand All @@ -528,10 +543,13 @@ mod compact {
let morph_receipt = MorphTransactionReceipt {
inner,
l1_fee,
version: version.map(|v| v as u8),
fee_token_id: fee_token_id.map(|id| id as u16),
fee_rate,
token_scale,
fee_limit,
reference,
memo: memo.map(|m| m.into_owned()),
};

match tx_type {
Expand Down
79 changes: 77 additions & 2 deletions crates/primitives/src/receipt/receipt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,15 @@ use alloy_rlp::{BufMut, Decodable, Encodable, Header};
///
/// This receipt extends the standard Ethereum receipt with:
/// - `l1_fee`: The L1 data fee charged for posting transaction data to L1
/// - `version`: The version of the Morph transaction format
/// - `fee_token_id`: The ERC20 token ID used for fee payment (TxMorph)
/// - `fee_rate`: The exchange rate for the fee token
/// - `token_scale`: The scale factor for the token
/// - `fee_limit`: The fee limit for TxMorph
/// - `reference`: The reference key for the transaction
/// - `memo`: The memo field for arbitrary data
///
/// Reference: <https://github.com/morph-l2/go-ethereum/pull/282>
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
Expand All @@ -32,6 +37,14 @@ pub struct MorphTransactionReceipt<T = Log> {
)]
pub l1_fee: Option<U256>,

/// The version of the Morph transaction format.
/// Only present for TxMorph.
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub version: Option<u8>,

/// The ERC20 token ID used for fee payment (TxMorph feature).
/// Only present for TxMorph.
#[cfg_attr(
Expand Down Expand Up @@ -63,6 +76,23 @@ pub struct MorphTransactionReceipt<T = Log> {
serde(default, skip_serializing_if = "Option::is_none")
)]
pub fee_limit: Option<U256>,

/// Reference key for the transaction.
/// Used for indexing and looking up transactions by external systems.
/// Only present for TxMorph.
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub reference: Option<alloy_primitives::B256>,

/// Memo field for arbitrary data.
/// Only present for TxMorph.
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub memo: Option<alloy_primitives::Bytes>,
}

impl<T> MorphTransactionReceipt<T> {
Expand All @@ -71,10 +101,13 @@ impl<T> MorphTransactionReceipt<T> {
Self {
inner,
l1_fee: None,
version: None,
fee_token_id: None,
fee_rate: None,
token_scale: None,
fee_limit: None,
reference: None,
memo: None,
}
}

Expand All @@ -83,14 +116,17 @@ impl<T> MorphTransactionReceipt<T> {
Self {
inner,
l1_fee: Some(l1_fee),
version: None,
fee_token_id: None,
fee_rate: None,
token_scale: None,
fee_limit: None,
reference: None,
memo: None,
}
}

/// Creates a new receipt with TxMorph fields.
/// Creates a new receipt with TxMorph fields (legacy version without reference/memo).
pub const fn with_morph_tx(
inner: Receipt<T>,
l1_fee: U256,
Expand All @@ -102,22 +138,61 @@ impl<T> MorphTransactionReceipt<T> {
Self {
inner,
l1_fee: Some(l1_fee),
version: None,
fee_token_id: Some(fee_token_id),
fee_rate: Some(fee_rate),
token_scale: Some(token_scale),
fee_limit: Some(fee_limit),
reference: None,
memo: None,
}
}

/// Creates a new receipt with all TxMorph fields including version, reference, and memo.
#[allow(clippy::too_many_arguments)]
pub const fn with_morph_tx_full(
inner: Receipt<T>,
l1_fee: U256,
version: u8,
fee_token_id: u16,
fee_rate: U256,
token_scale: U256,
fee_limit: U256,
reference: Option<alloy_primitives::B256>,
memo: Option<alloy_primitives::Bytes>,
) -> Self {
Self {
inner,
l1_fee: Some(l1_fee),
version: Some(version),
fee_token_id: Some(fee_token_id),
fee_rate: Some(fee_rate),
token_scale: Some(token_scale),
fee_limit: Some(fee_limit),
reference,
memo,
}
}

/// Returns true if this receipt is for a TxMorph.
pub const fn is_morph_tx(&self) -> bool {
self.fee_token_id.is_some()
self.fee_token_id.is_some() || self.version.is_some() || self.reference.is_some()
}

/// Returns true if this receipt has a reference.
pub const fn has_reference(&self) -> bool {
self.reference.is_some()
}
Comment on lines 177 to 185
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Include memo in is_morph_tx detection.

A receipt with only memo set would be treated as non-Morph. Including it keeps the predicate robust to future construction paths.

🛠️ Suggested update
-        self.fee_token_id.is_some() || self.version.is_some() || self.reference.is_some()
+        self.fee_token_id.is_some()
+            || self.version.is_some()
+            || self.reference.is_some()
+            || self.memo.is_some()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// Returns true if this receipt is for a TxMorph.
pub const fn is_morph_tx(&self) -> bool {
self.fee_token_id.is_some()
self.fee_token_id.is_some() || self.version.is_some() || self.reference.is_some()
}
/// Returns true if this receipt has a reference.
pub const fn has_reference(&self) -> bool {
self.reference.is_some()
}
/// Returns true if this receipt is for a TxMorph.
pub const fn is_morph_tx(&self) -> bool {
self.fee_token_id.is_some()
|| self.version.is_some()
|| self.reference.is_some()
|| self.memo.is_some()
}
/// Returns true if this receipt has a reference.
pub const fn has_reference(&self) -> bool {
self.reference.is_some()
}
🤖 Prompt for AI Agents
In `@crates/primitives/src/receipt/receipt.rs` around lines 177 - 185, The
is_morph_tx predicate currently checks fee_token_id, version, and reference but
misses memo, so receipts with only memo set are not treated as Morph; update the
pub const fn is_morph_tx(&self) -> bool in receipt.rs to also include
self.memo.is_some() in the boolean expression (alongside
self.fee_token_id.is_some(), self.version.is_some(), and
self.reference.is_some()) so memo presence is considered when detecting a
TxMorph.


/// Returns the L1 fee, defaulting to zero if not set.
pub fn l1_fee_or_zero(&self) -> U256 {
self.l1_fee.unwrap_or(U256::ZERO)
}

/// Returns the version, defaulting to 0 if not set.
pub fn version_or_zero(&self) -> u8 {
self.version.unwrap_or(0)
}
}

impl MorphTransactionReceipt {
Expand Down
4 changes: 3 additions & 1 deletion crates/primitives/src/transaction/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ pub mod morph_transaction;

pub use envelope::{MorphTxEnvelope, MorphTxType};
pub use l1_transaction::{L1_TX_TYPE_ID, TxL1Msg};
pub use morph_transaction::{MORPH_TX_TYPE_ID, TxMorph, TxMorphExt};
pub use morph_transaction::{
MAX_MEMO_LENGTH, MORPH_TX_TYPE_ID, MORPH_TX_VERSION_0, MORPH_TX_VERSION_1, TxMorph, TxMorphExt,
};
Loading