Skip to content

Commit

Permalink
[multi-chain] Implement a generic chain spec (#653)
Browse files Browse the repository at this point in the history
* feat(wip): Introduce a generic chain spec in the `edr_generic` crate

* feat: Allow for missing nonce/mixHash in remote blocks

* fixup: Formatting

* fix: Make sure to accept rpc blocks with txs being 32-byte hashes

* fixup: Maintain symmetry between mod::ConversionErrors used internally

* Update crates/edr_generic/src/lib.rs

Co-authored-by: Wodann <Wodann@users.noreply.github.com>

* Update crates/edr_generic/src/rpc/block.rs

Co-authored-by: Wodann <Wodann@users.noreply.github.com>

* fixup: Rename SignedWithFallbackToPostEip155

* feat: Implement ProviderSpec for GenericChainSpec

* feat: Implement SyncNapiSpec for GenericChainSpec and use in `edr_napi`

* fixup: Use the generic chain spec in the local Hardhat patch

* fixup: Re-generate patch hash

* fixup: Update test/provider.ts

* ci: run EDR TS tests

* ci: add Alchemy URL

* revert: deletion of L1 chain in N-API

* ci: cache RPC calls in TS tests

* refactor: Prefer const in pattern match over a guard in tx RLP decode

This is more consistent with the other uses and increases visibility to
the underlying static str value.

---------

Co-authored-by: Wodann <Wodann@users.noreply.github.com>
  • Loading branch information
Xanewok and Wodann authored Sep 13, 2024
1 parent b694d46 commit 05a8cb6
Show file tree
Hide file tree
Showing 34 changed files with 1,687 additions and 106 deletions.
45 changes: 45 additions & 0 deletions .github/workflows/edr-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,51 @@ jobs:
# command: nextest
# args: run --workspace --all-features --all-targets

test-edr-ts:
name: Test EDR TS bindings (${{ matrix.os }})
runs-on: ${{ matrix.os }}
needs: check-edr
strategy:
fail-fast: false
matrix:
node: [18.15]
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v3

- uses: pnpm/action-setup@v4
with:
version: 9

- name: Install Node
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
cache: pnpm

- name: Install Rust (stable)
uses: actions-rs/toolchain@v1
with:
profile: minimal
override: true

- uses: Swatinem/rust-cache@v2

- name: Cache EDR RPC cache
uses: actions/cache@v2
with:
path: |
**/edr-cache
key: edr-ts-rpc-cache-v1

- name: Install package
run: pnpm install --frozen-lockfile --prefer-offline

- name: Run tests
env:
ALCHEMY_URL: ${{ secrets.ALCHEMY_URL }}
run: cd crates/edr_napi && pnpm test

edr-style:
name: Check EDR Style
runs-on: ubuntu-latest
Expand Down
27 changes: 27 additions & 0 deletions Cargo.lock

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

4 changes: 0 additions & 4 deletions crates/edr_eth/src/transaction/signed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,6 @@ impl Signed {
_ => None,
}
}

pub fn is_invalid_transaction_type_error(message: &str) -> bool {
message == INVALID_TX_TYPE_ERROR_MESSAGE
}
}

impl alloy_rlp::Decodable for Signed {
Expand Down
11 changes: 11 additions & 0 deletions crates/edr_evm/src/hardfork.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@ impl<ChainSpecT: ChainSpec> Activations<ChainSpecT> {
})
.map(|entry| entry.1)
}

/// Views the activations as for a different chain spec (that shares the
/// underlying hardforks).
pub fn as_chain_spec<OtherChainSpecT: ChainSpec<Hardfork = ChainSpecT::Hardfork>>(
&'static self,
) -> &'static Activations<OtherChainSpecT> {
// SAFETY: The layout is the same as we're using the same struct and the
// Hardfork associated type is the same and we are also converting from
// one static reference to another, so no lifetime hazards here as well.
unsafe { std::mem::transmute(self) }
}
}

impl<ChainSpecT: ChainSpec> From<&[(ForkCondition, ChainSpecT::Hardfork)]>
Expand Down
50 changes: 0 additions & 50 deletions crates/edr_evm/tests/optimism.rs

This file was deleted.

37 changes: 37 additions & 0 deletions crates/edr_generic/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
[package]
name = "edr_generic"
version = "0.3.5"
edition = "2021"
description = "A generic chain specification for Ethereum Layer 1 chains."

[dependencies]
alloy-rlp = { version = "0.3", default-features = false, features = ["derive"] }
derive-where = { version = "1.2.7", default-features = false }
edr_eth = { path = "../edr_eth" }
edr_evm = { path = "../edr_evm" }
edr_provider = { path = "../edr_provider" }
edr_rpc_eth = { path = "../edr_rpc_eth" }
log = { version = "0.4.17", default-features = false }
revm = { git = "https://github.com/Wodann/revm", rev = "a500675", version = "12.1", default-features = false, features = ["c-kzg", "dev", "serde"] }
revm-primitives = { git = "https://github.com/Wodann/revm", rev = "a500675", version = "7.1", default-features = false, features = ["c-kzg", "hashbrown"] }
serde = { version = "1.0.209", default-features = false, features = ["derive"] }
thiserror = { version = "1.0.37", default-features = false }

[dev-dependencies]
anyhow = "1.0.75"
edr_defaults = { path = "../edr_defaults" }
edr_evm = { path = "../edr_evm", features = ["test-utils"] }
edr_rpc_eth = { path = "../edr_rpc_eth", features = ["test-utils"] }
edr_rpc_client = { path = "../edr_rpc_client" }
edr_test_utils = { path = "../edr_test_utils" }
parking_lot = { version = "0.12.1", default-features = false }
paste = { version = "1.0.14", default-features = false }
serde_json = { version = "1.0.127" }
serial_test = "2.0.0"
tokio = { version = "1.21.2", default-features = false, features = ["macros", "rt-multi-thread", "sync"] }

[features]
test-remote = []

[lints]
workspace = true
145 changes: 145 additions & 0 deletions crates/edr_generic/src/eip2718.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
use alloy_rlp::Buf as _;
use edr_eth::{
receipt::{MapReceiptLogs, Receipt, RootOrStatus},
transaction::TransactionType,
Bloom,
};

use crate::transaction;

/// An compile-time typed EIP-2718 envelope for L1 Ethereum.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum TypedEnvelope<DataT> {
/// Legacy transaction.
Legacy(DataT),
/// EIP-2930 transaction.
Eip2930(DataT),
/// EIP-1559 transaction.
Eip1559(DataT),
/// EIP-4844 transaction.
Eip4844(DataT),
/// Unrecognized transaction type.
Unrecognized(DataT),
}

impl<DataT> TypedEnvelope<DataT> {
/// Constructs a typed envelope around the given data.
pub fn new(data: DataT, transaction_type: transaction::Type) -> Self {
match transaction_type {
transaction::Type::Legacy => Self::Legacy(data),
transaction::Type::Eip2930 => Self::Eip2930(data),
transaction::Type::Eip1559 => Self::Eip1559(data),
transaction::Type::Eip4844 => Self::Eip4844(data),
transaction::Type::Unrecognized(_) => Self::Unrecognized(data),
}
}

/// Returns a reference to the data inside the envelope.
pub fn data(&self) -> &DataT {
match self {
TypedEnvelope::Legacy(data)
| TypedEnvelope::Eip2930(data)
| TypedEnvelope::Eip1559(data)
| TypedEnvelope::Eip4844(data)
| TypedEnvelope::Unrecognized(data) => data,
}
}

/// Maps the data inside the envelope to a new type.
pub fn map<NewDataT, F>(self, f: F) -> TypedEnvelope<NewDataT>
where
F: FnOnce(DataT) -> NewDataT,
{
match self {
TypedEnvelope::Legacy(data) => TypedEnvelope::Legacy(f(data)),
TypedEnvelope::Eip2930(data) => TypedEnvelope::Eip2930(f(data)),
TypedEnvelope::Eip1559(data) => TypedEnvelope::Eip1559(f(data)),
TypedEnvelope::Eip4844(data) => TypedEnvelope::Eip4844(f(data)),
TypedEnvelope::Unrecognized(data) => TypedEnvelope::Unrecognized(f(data)),
}
}
}

impl<DataT> TransactionType for TypedEnvelope<DataT> {
type Type = transaction::Type;

fn transaction_type(&self) -> Self::Type {
match self {
TypedEnvelope::Legacy(_) => transaction::Type::Legacy,
TypedEnvelope::Eip2930(_) => transaction::Type::Eip2930,
TypedEnvelope::Eip1559(_) => transaction::Type::Eip1559,
TypedEnvelope::Eip4844(_) => transaction::Type::Eip4844,
// TODO: Should we properly decode the transaction type?
TypedEnvelope::Unrecognized(_) => transaction::Type::Unrecognized(0xFF),
}
}
}

impl<DataT> alloy_rlp::Decodable for TypedEnvelope<DataT>
where
DataT: alloy_rlp::Decodable,
{
fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
fn is_list(byte: u8) -> bool {
byte >= 0xc0
}

let first = *buf.first().ok_or(alloy_rlp::Error::InputTooShort)?;
let transaction_type = if is_list(first) {
transaction::Type::Legacy
} else {
// Consume the first byte
buf.advance(1);

crate::transaction::Type::from(first)
};

let data = DataT::decode(buf)?;
Ok(TypedEnvelope::new(data, transaction_type))
}
}

impl<DataT> alloy_rlp::Encodable for TypedEnvelope<DataT>
where
DataT: alloy_rlp::Encodable,
{
fn encode(&self, out: &mut dyn alloy_rlp::BufMut) {
let transaction_type: u8 = self.transaction_type().into();
if transaction_type > 0 {
out.put_u8(transaction_type);
}

self.data().encode(out);
}

fn length(&self) -> usize {
let type_length = usize::from(u8::from(self.transaction_type()) > 0u8);
type_length + self.data().length()
}
}

impl<DataT: Receipt<LogT>, LogT> Receipt<LogT> for TypedEnvelope<DataT> {
fn cumulative_gas_used(&self) -> u64 {
self.data().cumulative_gas_used()
}

fn logs_bloom(&self) -> &Bloom {
self.data().logs_bloom()
}

fn transaction_logs(&self) -> &[LogT] {
self.data().transaction_logs()
}

fn root_or_status(&self) -> RootOrStatus<'_> {
self.data().root_or_status()
}
}

impl<OldDataT: MapReceiptLogs<OldLogT, NewLogT, NewDataT>, OldLogT, NewLogT, NewDataT>
MapReceiptLogs<OldLogT, NewLogT, TypedEnvelope<NewDataT>> for TypedEnvelope<OldDataT>
{
fn map_logs(self, map_fn: impl FnMut(OldLogT) -> NewLogT) -> TypedEnvelope<NewDataT> {
self.map(|data| data.map_logs(map_fn))
}
}
18 changes: 18 additions & 0 deletions crates/edr_generic/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//! A slightly more flexible chain specification for Ethereum Layer 1 chain.
mod eip2718;
mod receipt;
mod rpc;
mod spec;
mod transaction;

/// The chain specification for Ethereum Layer 1 that is a bit more lenient
/// and allows for more flexibility in contrast to
/// [`L1ChainSpec`](edr_eth::chain_spec::L1ChainSpec).
///
/// Specifically:
/// - it allows unknown transaction types (treats them as legacy
/// [`Eip155`](edr_eth::transaction::signed::Eip155) transactions)
/// - it allows remote blocks with missing `nonce` and `mix_hash` fields
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, alloy_rlp::RlpEncodable)]
pub struct GenericChainSpec;
3 changes: 3 additions & 0 deletions crates/edr_generic/src/receipt.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub use edr_eth::receipt::BlockReceipt;

pub mod execution;
Loading

0 comments on commit 05a8cb6

Please sign in to comment.