Skip to content
Merged
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions bin/morph-reth/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ reth-node-builder.workspace = true
reth-rpc-server-types.workspace = true

# Other
alloy-primitives.workspace = true
clap.workspace = true
eyre.workspace = true
tracing.workspace = true
Expand Down
103 changes: 61 additions & 42 deletions bin/morph-reth/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ use reth_cli_util::allocator::tikv_jemalloc_sys as _;
#[unsafe(export_name = "malloc_conf")]
static MALLOC_CONF: &[u8] = b"prof:true,prof_active:true,lg_prof_sample:19\0";

use alloy_primitives::U256;
use clap::Parser;
use morph_chainspec::{MorphChainSpec, MorphChainSpecParser};
use morph_chainspec::{MORPH_DEFAULT_PRIORITY_FEE, MorphChainSpec, MorphChainSpecParser};
use morph_consensus::MorphConsensus;
use morph_evm::{MorphEvmConfig, evm::MorphEvmFactory};
use morph_node::{
Expand All @@ -26,12 +27,28 @@ use morph_node::{
use morph_reference_index::ReferenceIndexDb;
use reth_chainspec::EthChainSpec;
use reth_cli_util::sigsegv_handler;
use reth_ethereum_cli::Cli;
use reth_ethereum_cli::{Cli, Commands};
use reth_node_builder::Node;
use reth_rpc_server_types::DefaultRpcModuleValidator;
use std::sync::Arc;
use tracing::info;

fn morph_default_suggested_fee() -> U256 {
U256::from_limbs([MORPH_DEFAULT_PRIORITY_FEE, 0, 0, 0])
}

fn apply_morph_cli_defaults(
cli: &mut Cli<MorphChainSpecParser, MorphArgs, DefaultRpcModuleValidator>,
) {
if let Commands::Node(command) = &mut cli.command {
command
.rpc
.gas_price_oracle
.default_suggested_fee
.get_or_insert_with(morph_default_suggested_fee);
}
}

fn main() {
// Override reth's default version info with morph-reth's own version,
// commit SHA, and build timestamp. Must be called before CLI parsing.
Expand All @@ -55,47 +72,49 @@ fn main() {
)
};

// Parse CLI arguments and run the node
let mut cli = Cli::<MorphChainSpecParser, MorphArgs, DefaultRpcModuleValidator>::parse();
apply_morph_cli_defaults(&mut cli);

// Run the node
if let Err(err) =
Cli::<MorphChainSpecParser, MorphArgs, DefaultRpcModuleValidator>::parse()
.run_with_components::<MorphNode>(components, async move |builder, morph_args| {
info!(target: "morph::cli", "Starting Morph-Reth node");

// Open the reference index DB before launching the node so we
// can wire it into both the ExEx and the add-ons.
let chain_spec = builder.config().chain.clone();
let datadir = builder.config().datadir();
let reference_index_path = datadir.data_dir().join("morph").join("reference_index");
let chain_id = chain_spec.chain().id();
let genesis_hash = chain_spec.genesis_hash(); // from EthChainSpec trait

info!(
target: "morph::reference_index",
path = %reference_index_path.display(),
chain_id,
"opening Morph reference index database"
);
let db = ReferenceIndexDb::open(&reference_index_path, chain_id, genesis_hash)?;
let (control, startup_rx) = ReferenceIndexControl::new(db);

let exex_control = control.clone();
let node = MorphNode::new(morph_args);

let handle = builder
.with_types::<MorphNode>()
.with_components(node.components_builder())
.with_add_ons(MorphAddOns::new().with_reference_index(control))
.install_exex("morph-reference-index", async move |ctx| {
Ok(reference_index_exex(ctx, exex_control, startup_rx))
})
.launch_with_debug_capabilities()
.await?;

info!(target: "morph::cli", "Node started successfully");

// Wait for node exit
handle.node_exit_future.await
})
cli.run_with_components::<MorphNode>(components, async move |builder, morph_args| {
info!(target: "morph::cli", "Starting Morph-Reth node");

// Open the reference index DB before launching the node so we
// can wire it into both the ExEx and the add-ons.
let chain_spec = builder.config().chain.clone();
let datadir = builder.config().datadir();
let reference_index_path = datadir.data_dir().join("morph").join("reference_index");
let chain_id = chain_spec.chain().id();
let genesis_hash = chain_spec.genesis_hash(); // from EthChainSpec trait

info!(
target: "morph::reference_index",
path = %reference_index_path.display(),
chain_id,
"opening Morph reference index database"
);
let db = ReferenceIndexDb::open(&reference_index_path, chain_id, genesis_hash)?;
let (control, startup_rx) = ReferenceIndexControl::new(db);

let exex_control = control.clone();
let node = MorphNode::new(morph_args);

let handle = builder
.with_types::<MorphNode>()
.with_components(node.components_builder())
.with_add_ons(MorphAddOns::new().with_reference_index(control))
.install_exex("morph-reference-index", async move |ctx| {
Ok(reference_index_exex(ctx, exex_control, startup_rx))
})
.launch_with_debug_capabilities()
.await?;

info!(target: "morph::cli", "Node started successfully");

// Wait for node exit
handle.node_exit_future.await
})
{
eprintln!("Error: {err:?}");
std::process::exit(1);
Expand Down
11 changes: 11 additions & 0 deletions crates/chainspec/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@ pub const MORPH_HOODI_CHAIN_ID: u64 = 2910;
/// The sequencer has the right to set any base fee below `MORPH_MAX_BASE_FEE`.
pub const MORPH_BASE_FEE: u64 = 1_000_000;

/// Default priority fee returned by `eth_maxPriorityFeePerGas` when the gas
/// price oracle has no usable block samples (cold start or empty/zero-tip
/// blocks, the common case on Morph L2).
///
/// Matches morph-geth, which inherits this value from upstream go-ethereum's
/// `miner.DefaultConfig.GasPrice = params.GWei / 1000 = 1_000_000 wei`
/// (see `eth/backend.go`: `gpoParams.Default = config.Miner.GasPrice`).
/// Without this default, reth would fall back to its own 1 gwei default,
/// causing `eth_maxPriorityFeePerGas` to diverge from geth by 1000x.
pub const MORPH_DEFAULT_PRIORITY_FEE: u64 = 1_000_000;

/// Morph Mainnet genesis hash (computed with ZK-trie state root).
///
/// Source: go-ethereum/params/config.go
Expand Down
6 changes: 4 additions & 2 deletions crates/node/tests/it/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,8 @@ async fn transaction_receipt_exposes_morph_fields_over_rpc() -> eyre::Result<()>
.await?;

assert_eq!(receipt["type"].as_str(), Some("0x7f"));
assert_eq!(receipt["version"].as_u64(), Some(1));
// version is JSON-RPC quantity-encoded (string "0x1"), not a number.
assert_eq!(receipt["version"].as_str(), Some("0x1"));
assert_eq!(receipt["feeTokenID"].as_str(), Some("0x1"));
assert_eq!(
receipt["reference"].as_str(),
Expand Down Expand Up @@ -506,7 +507,8 @@ async fn transaction_by_hash_exposes_morph_fields_over_rpc() -> eyre::Result<()>

assert_eq!(tx["hash"].as_str(), Some(tx_hash.to_string().as_str()));
assert_eq!(tx["type"].as_str(), Some("0x7f"));
assert_eq!(tx["version"].as_u64(), Some(1));
// version is JSON-RPC quantity-encoded (string "0x1"), not a number.
assert_eq!(tx["version"].as_str(), Some("0x1"));
assert_eq!(tx["feeTokenID"].as_str(), Some("0x1"));
assert!(tx["feeLimit"].as_str().is_some());
assert_eq!(tx["reference"].as_str(), Some(expected_reference.as_str()));
Expand Down
55 changes: 51 additions & 4 deletions crates/primitives/src/transaction/morph_transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,27 @@ pub const MORPH_TX_VERSION_1: u8 = 1;
/// Maximum length of the memo field in bytes.
pub const MAX_MEMO_LENGTH: usize = 64;

#[cfg(feature = "serde")]
fn is_morph_tx_version_0(version: &u8) -> bool {
*version == MORPH_TX_VERSION_0
}

/// Canonical MorphTx-specific fields shared across modules.
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct MorphTxFields {
#[cfg_attr(feature = "serde", serde(default, with = "alloy_serde::quantity"))]
pub version: u8,
#[cfg_attr(feature = "serde", serde(default, with = "alloy_serde::quantity"))]
#[cfg_attr(
feature = "serde",
serde(
default,
with = "alloy_serde::quantity",
rename = "feeTokenID",
alias = "feeTokenId"
)
)]
Comment thread
coderabbitai[bot] marked this conversation as resolved.
pub fee_token_id: u16,
#[cfg_attr(feature = "serde", serde(default))]
pub fee_limit: U256,
Expand Down Expand Up @@ -78,7 +91,10 @@ pub struct TxMorph {
/// in executing this transaction. This is paid up-front, before any
/// computation is done and may not be increased later.
/// Matches go-ethereum's `AltFeeTx.Gas` (uint64).
#[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
#[cfg_attr(
feature = "serde",
serde(with = "alloy_serde::quantity", rename = "gas", alias = "gasLimit")
)]
pub gas_limit: u64,

/// A scalar value equal to the maximum amount of gas that should be used
Expand Down Expand Up @@ -113,13 +129,28 @@ pub struct TxMorph {

/// Version of the Morph transaction format.
/// Used for future extensibility.
#[cfg_attr(feature = "serde", serde(default, with = "alloy_serde::quantity"))]
#[cfg_attr(
feature = "serde",
serde(
default,
with = "alloy_serde::quantity",
skip_serializing_if = "is_morph_tx_version_0"
)
)]
pub version: u8,

/// Token ID for alternative fee payment.
/// This corresponds to the token registered in the L2 Token Registry.
/// 0 means ETH payment, > 0 means ERC20 token payment.
#[cfg_attr(feature = "serde", serde(default, with = "alloy_serde::quantity"))]
#[cfg_attr(
feature = "serde",
serde(
default,
with = "alloy_serde::quantity",
rename = "feeTokenID",
alias = "feeTokenId"
)
)]
pub fee_token_id: u16,

/// Maximum amount of tokens the sender is willing to pay as fee.
Expand Down Expand Up @@ -2142,6 +2173,22 @@ mod tests {
assert_eq!(fields.memo, None);
}

#[cfg(feature = "serde")]
#[test]
fn test_morph_tx_fields_serde_uses_canonical_fee_token_id_key() {
let fields = MorphTxFields {
version: 1,
fee_token_id: 7,
fee_limit: U256::from(999u64),
reference: None,
memo: None,
};

let json = serde_json::to_value(fields).unwrap();
assert_eq!(json.get("feeTokenID"), Some(&serde_json::json!("0x7")));
assert!(json.get("feeTokenId").is_none());
}

#[cfg(feature = "reth-codec")]
#[test]
fn test_compact_roundtrip_v1_with_memo() {
Expand Down
Loading