Skip to content

Commit

Permalink
feat: Initial implementation (#1)
Browse files Browse the repository at this point in the history
* cargo init + add reth dependency - failing to build

* Copy paste ci lint workflow from reth repo

* Copy example for CLI extension

* Move Cli extension and rpc implementation to separate modules

* Add implementation (not compiling)

* Fix build errors

* Change fork branch and rename MyRethCli to ValidationCli

* Cargo fmt

* Add test rpc payload

* Add README

* fix test data

* Extend README

* Add Disclaimer

* Remove references to txpool example

* Change heading of README
  • Loading branch information
ckoopmann authored Sep 27, 2023
1 parent 1232cda commit 8f1f00c
Show file tree
Hide file tree
Showing 9 changed files with 605 additions and 0 deletions.
48 changes: 48 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
on:
pull_request:
merge_group:
push:
branches: [main]

env:
RUSTFLAGS: -D warnings
CARGO_TERM_COLOR: always

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

name: ci
jobs:
lint:
name: code lint
runs-on: ubuntu-20.04
steps:
- name: Checkout sources
uses: actions/checkout@v3
- name: Install toolchain
uses: dtolnay/rust-toolchain@nightly
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true

- name: cargo check
uses: actions-rs/cargo@v1
with:
command: check
args: --all --all-features --benches --tests

- name: cargo fmt
uses: actions-rs/cargo@v1
with:
command: fmt
args: --all --check

- name: cargo clippy
uses: actions-rs/cargo@v1
with:
command: clippy
args: --all --all-features --benches --tests

5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,8 @@ Cargo.lock

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


# Added by cargo

/target
16 changes: 16 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "reth-block-validator"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
async-trait = "0.1.73"
clap = "4.4.5"
derivative = "2.2.0"
eyre = "0.6.8"
jsonrpsee = "0.20.1"
reth = { git = "https://github.com/ultrasoundmoney/reth-block-validator", branch = "changes-to-enable-validation-api-extension" }
serde = "1.0.188"
serde-this-or-that = "0.4.2"
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Reth based Builder Payload Validator Api
Reth rpc extension to add an endpoint for validation of builder submissions as received by a relay.

## Get Started
Run extended reth full node with the added rpc endpoint with:
`RUST_LOG=info cargo run -- node --full --metrics 127.0.0.1:9001 --http --enable-ext`

## Test it
While there are no automated tests yet you can execute a manual test using the provided testdata:
`curl --location 'localhost:8545/' --header 'Content-Type: application/json' --data @test/data/rpc_payload.json`

## Further Reading
- [Guide to custom api development based on reth](https://www.libevm.com/2023/09/01/reth-custom-api/)
- [Official example for adding rpc namespace](https://github.com/paradigmxyz/reth/blob/main/examples/additional-rpc-namespace-in-cli/src/main.rs)

## Disclaimer
This code is being provided as is. No guarantee, representation or warranty is being made, express or implied, as to the safety or correctness of the code. It has not been audited and as such there can be no assurance it will work as intended, and users may experience delays, failures, errors, omissions or loss of transmitted information.





73 changes: 73 additions & 0 deletions src/cli_ext.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
use reth::{
cli::{
config::RethRpcConfig,
ext::{RethCliExt, RethNodeCommandConfig},
},
network::{NetworkInfo, Peers},
providers::{
AccountReader, BlockReaderIdExt, CanonStateSubscriptions, ChainSpecProvider,
ChangeSetReader, EvmEnvProvider, StateProviderFactory,
},
rpc::builder::{RethModuleRegistry, TransportRpcModules},
tasks::TaskSpawner,
transaction_pool::TransactionPool,
};

use crate::rpc::ValidationApiServer;
use crate::ValidationApi;

/// The type that tells the reth CLI what extensions to use
pub struct ValidationCliExt;

impl RethCliExt for ValidationCliExt {
/// This tells the reth CLI to install the `validation` rpc namespace via `RethCliValidationApi`
type Node = RethCliValidationApi;
}

/// Our custom cli args extension that adds one flag to reth default CLI.
#[derive(Debug, Clone, Copy, Default, clap::Args)]
pub struct RethCliValidationApi {
/// CLI flag to enable the validation extension namespace
#[clap(long)]
pub enable_ext: bool,
}

impl RethNodeCommandConfig for RethCliValidationApi {
// This is the entrypoint for the CLI to extend the RPC server with custom rpc namespaces.
fn extend_rpc_modules<Conf, Provider, Pool, Network, Tasks, Events>(
&mut self,
_config: &Conf,
registry: &mut RethModuleRegistry<Provider, Pool, Network, Tasks, Events>,
modules: &mut TransportRpcModules,
) -> eyre::Result<()>
where
Conf: RethRpcConfig,
Provider: BlockReaderIdExt
+ AccountReader
+ StateProviderFactory
+ EvmEnvProvider
+ ChainSpecProvider
+ ChangeSetReader
+ Clone
+ Unpin
+ 'static,
Pool: TransactionPool + Clone + 'static,
Network: NetworkInfo + Peers + Clone + 'static,
Tasks: TaskSpawner + Clone + 'static,
Events: CanonStateSubscriptions + Clone + 'static,
{
if !self.enable_ext {
return Ok(());
}

// here we get the configured pool type from the CLI.
let provider = registry.provider().clone();
let ext = ValidationApi::new(provider);

// now we merge our extension namespace into all configured transports
modules.merge_configured(ext.into_rpc())?;

println!("validation extension enabled");
Ok(())
}
}
32 changes: 32 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//! Reth RPC extension to add endpoint for builder payload validation
//!
//! Run with
//!
//! ```not_rust
//! RUST_LOG=info cargo run -- node --full --metrics 127.0.0.1:9001 --http --enable-ext
//! ```
//!
//! This installs an additional RPC method that can be queried using the provided sample rpc
//! payload
//!
//! ```sh
//! curl --location 'localhost:8545/' --header 'Content-Type: application/json' --data @test/data/rpc_payload.json
//! ```
use clap::Parser;
use reth::cli::Cli;
use std::sync::Arc;

mod cli_ext;
use cli_ext::ValidationCliExt;

mod rpc;
use rpc::ValidationApiInner;

fn main() {
Cli::<ValidationCliExt>::parse().run().unwrap();
}

/// The type that implements the `validation` rpc namespace trait
pub struct ValidationApi<Provider> {
inner: Arc<ValidationApiInner<Provider>>,
}
86 changes: 86 additions & 0 deletions src/rpc/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
use async_trait::async_trait;
use jsonrpsee::{core::RpcResult, proc_macros::rpc};

use reth::consensus_common::validation::full_validation;
use reth::providers::{
AccountReader, BlockReaderIdExt, ChainSpecProvider, ChangeSetReader, HeaderProvider,
StateProviderFactory, WithdrawalsProvider,
};
use reth::rpc::result::ToRpcResultExt;
use reth::rpc::types_compat::engine::payload::try_into_sealed_block;

use std::sync::Arc;

use crate::ValidationApi;

mod types;
use types::ExecutionPayloadValidation;

/// trait interface for a custom rpc namespace: `validation`
///
/// This defines an additional namespace where all methods are configured as trait functions.
#[rpc(server, namespace = "validationExt")]
#[async_trait]
pub trait ValidationApi {
/// Validates a block submitted to the relay
#[method(name = "validateBuilderSubmissionV1")]
async fn validate_builder_submission_v1(
&self,
execution_payload: ExecutionPayloadValidation,
) -> RpcResult<()>;
}

impl<Provider> ValidationApi<Provider> {
/// The provider that can interact with the chain.
pub fn provider(&self) -> &Provider {
&self.inner.provider
}

/// Create a new instance of the [ValidationApi]
pub fn new(provider: Provider) -> Self {
let inner = Arc::new(ValidationApiInner { provider });
Self { inner }
}
}

#[async_trait]
impl<Provider> ValidationApiServer for ValidationApi<Provider>
where
Provider: BlockReaderIdExt
+ ChainSpecProvider
+ ChangeSetReader
+ StateProviderFactory
+ HeaderProvider
+ AccountReader
+ WithdrawalsProvider
+ 'static,
{
/// Validates a block submitted to the relay
async fn validate_builder_submission_v1(
&self,
execution_payload: ExecutionPayloadValidation,
) -> RpcResult<()> {
let block = try_into_sealed_block(execution_payload.into(), None).map_ok_or_rpc_err()?;
let chain_spec = self.provider().chain_spec();
full_validation(&block, self.provider(), &chain_spec).map_ok_or_rpc_err()
}
}

impl<Provider> std::fmt::Debug for ValidationApi<Provider> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ValidationApi").finish_non_exhaustive()
}
}

impl<Provider> Clone for ValidationApi<Provider> {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
}
}
}

pub struct ValidationApiInner<Provider> {
/// The provider that can interact with the chain.
provider: Provider,
}
85 changes: 85 additions & 0 deletions src/rpc/types.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
use derivative::Derivative;
use reth::primitives::{Address, Bloom, Bytes, H256, U256};
use reth::rpc::types::{ExecutionPayload, ExecutionPayloadV1, ExecutionPayloadV2, Withdrawal};
use serde::{Deserialize, Serialize};
use serde_this_or_that::as_u64;

/// Structure to deserialize execution payloads sent according to the builder api spec
/// Numeric fields deserialized as decimals (unlike crate::eth::engine::ExecutionPayload)
#[derive(Derivative)]
#[derivative(Debug)]
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[allow(missing_docs)]
pub struct ExecutionPayloadValidation {
pub parent_hash: H256,
pub fee_recipient: Address,
pub state_root: H256,
pub receipts_root: H256,
pub logs_bloom: Bloom,
pub prev_randao: H256,
#[serde(deserialize_with = "as_u64")]
pub block_number: u64,
#[serde(deserialize_with = "as_u64")]
pub gas_limit: u64,
#[serde(deserialize_with = "as_u64")]
pub gas_used: u64,
#[serde(deserialize_with = "as_u64")]
pub timestamp: u64,
pub extra_data: Bytes,
pub base_fee_per_gas: U256,
pub block_hash: H256,
#[derivative(Debug = "ignore")]
pub transactions: Vec<Bytes>,
pub withdrawals: Vec<WithdrawalValidation>,
}

/// Withdrawal object with numbers deserialized as decimals
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct WithdrawalValidation {
/// Monotonically increasing identifier issued by consensus layer.
#[serde(deserialize_with = "as_u64")]
pub index: u64,
/// Index of validator associated with withdrawal.
#[serde(deserialize_with = "as_u64")]
pub validator_index: u64,
/// Target address for withdrawn ether.
pub address: Address,
/// Value of the withdrawal in gwei.
#[serde(deserialize_with = "as_u64")]
pub amount: u64,
}

impl From<ExecutionPayloadValidation> for ExecutionPayload {
fn from(val: ExecutionPayloadValidation) -> Self {
ExecutionPayload::V2(ExecutionPayloadV2 {
payload_inner: ExecutionPayloadV1 {
parent_hash: val.parent_hash,
fee_recipient: val.fee_recipient,
state_root: val.state_root,
receipts_root: val.receipts_root,
logs_bloom: val.logs_bloom,
prev_randao: val.prev_randao,
block_number: val.block_number.into(),
gas_limit: val.gas_limit.into(),
gas_used: val.gas_used.into(),
timestamp: val.timestamp.into(),
extra_data: val.extra_data,
base_fee_per_gas: val.base_fee_per_gas,
block_hash: val.block_hash,
transactions: val.transactions,
},
withdrawals: val.withdrawals.into_iter().map(|w| w.into()).collect(),
})
}
}

impl From<WithdrawalValidation> for Withdrawal {
fn from(val: WithdrawalValidation) -> Self {
Withdrawal {
index: val.index,
validator_index: val.validator_index,
address: val.address,
amount: val.amount,
}
}
}
Loading

0 comments on commit 8f1f00c

Please sign in to comment.