Skip to content
Merged
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
26 changes: 26 additions & 0 deletions .github/workflows/rust-clippy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Rust Clippy Check

on:
push:
branches:
- main
pull_request:

env:
CARGO_TERM_COLOR: always
# Make sure CI fails on all warnings, including Clippy lints
RUSTFLAGS: "-Dwarnings"

jobs:
clippy_check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- run: rustup component add clippy

- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2

- name: Run Clippy
run: cargo clippy --all-targets --all-features
29 changes: 29 additions & 0 deletions .github/workflows/rust-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Rust Tests

on:
push:
branches:
- main
pull_request:

env:
CARGO_TERM_COLOR: always

jobs:
test:
name: Run Rust Tests
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

# GitHub Actions supports rustup by default, so we can install the toolchain directly
# selecting a toolchain either by action or manual `rustup` calls should happen
# before the plugin, as the cache uses the current rustc version as its cache key
- run: rustup toolchain install stable --profile minimal

- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2

- name: Run tests
run: cargo test --all
3 changes: 3 additions & 0 deletions metabased-sequencer/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
./target/
./.github/
./Dockerfile
25 changes: 25 additions & 0 deletions metabased-sequencer/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Generated by Cargo
# will have compiled files and executables
debug/
target/

# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock

# These are backup files generated by rustfmt
**/*.rs.bk

# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb

# RustRover
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

# Added by cargo

/target
39 changes: 39 additions & 0 deletions metabased-sequencer/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
[workspace]
resolver = "2"
members = [
"interceptor",
"proxy",
]

[workspace.package]
version = "0.1.0"
edition = "2021"
rust-version = "1.81"

[workspace.dependencies]
alloy = { version = "0.3.1", features = ["full"] }
# TODO: Opt into specific features
jsonrpsee = { version = "0.24.3", features = ["full"] }
tokio = { version = "1.0", features = ["full"] }
anyhow = "1.0"
serde_json = "1.0.128"
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json"] }
tracing = "0.1.40"
clap = { version = "4.5.17", features = ["derive"] }
hyper = { version = "1.4.1", features = ["full"] }
bytes = "1.7.1"
http-body = "1.0.1"
http-body-util = "0.1"
pin-project-lite = "0.2.14"
http = "1.1.0"
want = { version = "0.3" }
smallvec = { version = "1.12", features = ["const_generics", "const_new"] }
futures-util = { version = "0.3", default-features = false }
futures-channel = { version = "0.3" }
httparse = { version = "1.8" }
itoa = { version = "1" }
h2 = { version = "0.4.2" }
httpdate = { version = "1.0" }
reqwest = "0.12.7"
serde = "1.0.210"
hyper-util = "0.1.8"
36 changes: 36 additions & 0 deletions metabased-sequencer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#NOTE: this builds `proxy.rs`
# Stage 1: Build the Rust application
FROM rust:latest as builder

# Set the working directory
WORKDIR /usr/src/metabased-sequencer

# Copy the entire project
COPY . .

# Build the proxy binary
RUN cargo build --release --bin proxy

# Stage 2: Create the runtime image
FROM debian:bookworm-slim

# Install required certificates for HTTPS requests and OpenSSL 3 for cryptography support
RUN apt-get update && apt-get install -y ca-certificates openssl libssl-dev && rm -rf /var/lib/apt/lists/*

# Set the working directory
WORKDIR /usr/local/bin

# Copy the compiled binary from the builder stage
COPY --from=builder /usr/src/metabased-sequencer/target/release/proxy .

# Ensure the binary has execute permissions
RUN chmod +x proxy

# Expose port 8456
EXPOSE 8456

# Set the entrypoint to the proxy binary
ENTRYPOINT ["./proxy"]

# Define default command-line arguments (optional)
CMD ["--proxy-address", "127.0.0.1:8456", "--target-address", "https://base-sepolia.blockpi.network/v1/rpc/public"]
2 changes: 2 additions & 0 deletions metabased-sequencer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# metabased-sequencer
The service for the write side of the metabased sequencer
33 changes: 33 additions & 0 deletions metabased-sequencer/interceptor/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
[package]
name = "interceptor"
version = "0.1.0"
edition = "2021"

[dependencies]
async-trait = "0.1"
alloy = { version = "0.4.2", features = ["full"] }
jsonrpsee = { version = "0.24.3", features = ["full"] }
tokio = { version = "1.0", features = ["full"] }
anyhow = "1.0"
serde_json = "1.0.128"
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json"] }
tracing = "0.1.40"
clap = { version = "4.5.17", features = ["derive"] }
hyper = { version = "1.4.1", features = ["full"] }
bytes = "1.7.1"
http-body = "1.0.1"
http-body-util = "0.1"
pin-project-lite = "0.2.14"
http = "1.1.0"
want = { version = "0.3" }
smallvec = { version = "1.12", features = ["const_generics", "const_new"] }
futures-util = { version = "0.3", default-features = false }
futures-channel = { version = "0.3" }
httparse = { version = "1.8" }
itoa = { version = "1" }
h2 = { version = "0.4.2" }
httpdate = { version = "1.0" }
reqwest = "0.12.7"
serde = "1.0.210"
hyper-util = "0.1.8"
alloy-primitives = "0.8.7"
3 changes: 3 additions & 0 deletions metabased-sequencer/interceptor/src/application/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mod send_raw_transaction;

pub use send_raw_transaction::send_raw_transaction;
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
use crate::presentation::json_rpc_errors::JsonRpcErrorCode;
use crate::presentation::json_rpc_errors::JsonRpcErrorCode::InvalidRequest;
use crate::presentation::jsonrpc::{rpc_error, JsonRpcError};
use crate::presentation::transaction;
use alloy::consensus::{Transaction, TxEnvelope, TxType};
use alloy::primitives::private::alloy_rlp::Decodable;
use alloy::primitives::TxHash;
use alloy_primitives::U256;
use bytes::Bytes;

/// Sends serialized and signed transaction `tx`.
pub fn send_raw_transaction(tx: Bytes) -> Result<TxHash, JsonRpcError<()>> {
// TODO remove or move up to function comment
// 1. Decoding
// 2. Validation
// 3. Submission/forwarding

// 1. Decoding:
let mut slice: &[u8] = tx.as_ref();
let tx = TxEnvelope::decode(&mut slice)?;

// 2. Validation:
tx.recover_signer().map_err(|_e| {
rpc_error(
"Invalid signer on transaction",
JsonRpcErrorCode::InvalidParams,
None,
)
})?;

if tx.tx_type() == TxType::Legacy {
// TODO(SEQ-179): introduce optional global tx cap config. See op-geth's checkTxFee() + RPCTxFeeCap for equivalent
// skip check if unset
let tx_cap_in_wei = U256::from(1_000_000_000_000_000_000u64); // 1e18wei = 1 ETH
let gas_price = tx.gas_price().ok_or_else(|| {
rpc_error("Legacy transaction missing gas price", InvalidRequest, None)
})?;
transaction::check_tx_fee(
U256::try_from(gas_price)?,
U256::try_from(tx.gas_limit())?,
tx_cap_in_wei,
)
.map_err(|_e| {
rpc_error(
"Transaction fee exceeds the configured cap",
JsonRpcErrorCode::InvalidInput,
None,
)
})?;
}

Ok(tx.tx_hash().to_owned())
}
11 changes: 11 additions & 0 deletions metabased-sequencer/interceptor/src/domain/chain.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use super::primitives::{Address, Bytes};
use jsonrpsee::core::async_trait;

#[async_trait]
pub trait MetabasedSequencerChainService {
async fn process_bulk_transactions(
&self,
account: Address,
tx: Vec<Bytes>,
) -> anyhow::Result<()>;
}
5 changes: 5 additions & 0 deletions metabased-sequencer/interceptor/src/domain/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pub mod primitives;

mod chain;

pub use chain::*;
1 change: 1 addition & 0 deletions metabased-sequencer/interceptor/src/domain/primitives.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub use alloy::primitives::{Address, Bytes};
1 change: 1 addition & 0 deletions metabased-sequencer/interceptor/src/infrastructure/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mod sol;
35 changes: 35 additions & 0 deletions metabased-sequencer/interceptor/src/infrastructure/sol.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use crate::domain::primitives::{Address, Bytes};
use crate::domain::MetabasedSequencerChainService;
use alloy::providers::Provider;
use alloy::sol;
use async_trait::async_trait;

sol! {
#[derive(Debug, PartialEq, Eq)]
#[sol(rpc)]
contract MetabasedSequencerChain {
event TransactionProcessed(address indexed sender, bytes encodedTxn);
error InvalidTransactionForm();
function emitTransactionProcessed(bytes calldata encodedTxn) public;
function processTransaction(bytes calldata encodedTxn) public;
function processBulkTransactions(bytes[] calldata encodedTxns) public;
}
}

#[async_trait]
impl<P> MetabasedSequencerChainService for P
where
P: Provider,
{
async fn process_bulk_transactions(
&self,
account: Address,
tx: Vec<Bytes>,
) -> anyhow::Result<()> {
let contract = MetabasedSequencerChain::new(account, self);

contract.processBulkTransactions(tx).call().await?;

Ok(())
}
}
4 changes: 4 additions & 0 deletions metabased-sequencer/interceptor/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub mod application;
pub mod domain;
pub mod infrastructure;
pub mod presentation;
7 changes: 7 additions & 0 deletions metabased-sequencer/interceptor/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
use interceptor::presentation::cli;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
cli::init_tracing_subscriber();
cli::run().await
}
20 changes: 20 additions & 0 deletions metabased-sequencer/interceptor/src/presentation/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use crate::presentation::server;
use tracing_subscriber::EnvFilter;

pub fn init_tracing_subscriber() {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.try_init()
.expect("setting default subscriber failed");
}

pub async fn run() -> anyhow::Result<()> {
let port = 8456;
let (addr, handle) = server::run(port).await?;
println!("RPC Server started on {}", addr);

// Keep the server running
handle.stopped().await;

Ok(())
}
Loading
Loading