Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial implementation #1

Merged
merged 15 commits into from
Sep 27, 2023
Merged
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 Block 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 `txpool` 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 txpool 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!("txpool extension enabled");
Ok(())
}
}
31 changes: 31 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//! Example of how to use additional rpc namespaces in the reth CLI
//!
//! Run with
//!
//! ```not_rust
//! cargo run -p additional-rpc-namespace-in-cli -- node --http --ws --enable-ext
//! ```
//!
//! This installs an additional RPC method `txpoolExt_transactionCount` that can queried via [cast](https://github.com/foundry-rs/foundry)
//!
//! ```sh
//! cast rpc txpoolExt_transactionCount
//! ```
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 `txpool` 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)]
Comment on lines +7 to +12
Copy link

Choose a reason for hiding this comment

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

Why do you need new types here? Seems like maintain headache, do we need to make the deser logic better?

Copy link
Collaborator Author

@ckoopmann ckoopmann Sep 27, 2023

Choose a reason for hiding this comment

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

This is really just because of difference in the formatting of the current api that we are using (snake_case and decimal encoded numbers).
I agree this is a headache and we should probably just use the existing formatting (camelCase and hex numbers), in which case we can drop these additional types and just use the existing ExecutionPayload struct.

@alextes What do you think ? (as is we will have to adjust the client code anyway because this api is differs from the existing one already)

Copy link
Collaborator Author

@ckoopmann ckoopmann Sep 27, 2023

Choose a reason for hiding this comment

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

If we do that I could probably also remove these re-exports from my pr against the reth repo. (if they are unwanted)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I opened an issue for this, and suggest doing this in a separate PR:
#2

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