Implement engine_getBlobsV1 (#9723)

Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
This commit is contained in:
Michael Sproul
2024-09-10 01:28:53 +10:00
committed by GitHub
parent c258c1547e
commit d8b12ac499
19 changed files with 184 additions and 21 deletions

4
Cargo.lock generated
View File

@ -8383,6 +8383,7 @@ dependencies = [
"reth-tasks",
"reth-testing-utils",
"reth-tokio-util",
"reth-transaction-pool",
"serde",
"thiserror",
"tokio",
@ -8497,6 +8498,7 @@ dependencies = [
name = "reth-rpc-types"
version = "1.0.6"
dependencies = [
"alloy-eips",
"alloy-primitives",
"alloy-rpc-types",
"alloy-rpc-types-admin",
@ -8513,6 +8515,7 @@ dependencies = [
"op-alloy-rpc-types",
"op-alloy-rpc-types-engine",
"rand 0.8.5",
"serde",
]
[[package]]
@ -8764,6 +8767,7 @@ dependencies = [
"reth-metrics",
"reth-primitives",
"reth-provider",
"reth-rpc-types",
"reth-storage-api",
"reth-tasks",
"reth-tracing",

View File

@ -267,6 +267,7 @@ where
ctx.chain_spec(),
beacon_engine_handle,
ctx.components().payload_builder().clone().into(),
ctx.components().pool().clone(),
Box::new(ctx.task_executor().clone()),
client,
EngineCapabilities::default(),

View File

@ -352,6 +352,7 @@ where
ctx.chain_spec(),
beacon_engine_handle,
ctx.components().payload_builder().clone().into(),
ctx.components().pool().clone(),
Box::new(ctx.task_executor().clone()),
client,
EngineCapabilities::default(),

View File

@ -14,8 +14,8 @@ use reth_rpc_types::{
ForkchoiceState, ForkchoiceUpdated, PayloadId, PayloadStatus, TransitionConfiguration,
},
state::StateOverride,
BlockOverrides, EIP1186AccountProofResponse, Filter, JsonStorageKey, Log, SyncStatus,
TransactionRequest,
BlobAndProofV1, BlockOverrides, EIP1186AccountProofResponse, Filter, JsonStorageKey, Log,
SyncStatus, TransactionRequest,
};
// NOTE: We can't use associated types in the `EngineApi` trait because of jsonrpsee, so we use a
// generic here. It would be nice if the rpc macro would understand which types need to have serde.
@ -213,6 +213,13 @@ pub trait EngineApi<Engine: EngineTypes> {
/// See also <https://github.com/ethereum/execution-apis/blob/6452a6b194d7db269bf1dbd087a267251d3cc7f8/src/engine/common.md#capabilities>
#[method(name = "exchangeCapabilities")]
async fn exchange_capabilities(&self, capabilities: Vec<String>) -> RpcResult<Vec<String>>;
/// Fetch blobs for the consensus layer from the in-memory blob cache.
#[method(name = "getBlobsV1")]
async fn get_blobs_v1(
&self,
transaction_ids: Vec<B256>,
) -> RpcResult<Vec<Option<BlobAndProofV1>>>;
}
/// A subset of the ETH rpc interface: <https://ethereum.github.io/execution-apis/api-documentation/>

View File

@ -17,7 +17,10 @@ use reth_rpc_layer::JwtSecret;
use reth_rpc_server_types::RpcModuleSelection;
use reth_rpc_types::engine::{ClientCode, ClientVersionV1};
use reth_tasks::TokioTaskExecutor;
use reth_transaction_pool::test_utils::{TestPool, TestPoolBuilder};
use reth_transaction_pool::{
noop::NoopTransactionPool,
test_utils::{TestPool, TestPoolBuilder},
};
use tokio::sync::mpsc::unbounded_channel;
/// Localhost with port 0 so a free port is used.
@ -43,6 +46,7 @@ pub async fn launch_auth(secret: JwtSecret) -> AuthServerHandle {
MAINNET.clone(),
beacon_engine_handle,
spawn_test_payload_service().into(),
NoopTransactionPool::default(),
Box::<TokioTaskExecutor>::default(),
client,
EngineCapabilities::default(),

View File

@ -25,6 +25,7 @@ reth-tasks.workspace = true
reth-rpc-types-compat.workspace = true
reth-engine-primitives.workspace = true
reth-evm.workspace = true
reth-transaction-pool.workspace = true
# async
tokio = { workspace = true, features = ["sync"] }

View File

@ -19,6 +19,7 @@ pub const CAPABILITIES: &[&str] = &[
"engine_getPayloadBodiesByRangeV1",
"engine_getPayloadBodiesByHashV2",
"engine_getPayloadBodiesByRangeV2",
"engine_getBlobsV1",
];
// The list of all supported Engine capabilities available over the engine endpoint.

View File

@ -16,17 +16,21 @@ use reth_primitives::{
Block, BlockHash, BlockHashOrNumber, BlockNumber, EthereumHardfork, B256, U64,
};
use reth_rpc_api::EngineApiServer;
use reth_rpc_types::engine::{
CancunPayloadFields, ClientVersionV1, ExecutionPayload, ExecutionPayloadBodiesV1,
ExecutionPayloadBodiesV2, ExecutionPayloadInputV2, ExecutionPayloadV1, ExecutionPayloadV3,
ExecutionPayloadV4, ForkchoiceState, ForkchoiceUpdated, PayloadId, PayloadStatus,
TransitionConfiguration,
use reth_rpc_types::{
engine::{
CancunPayloadFields, ClientVersionV1, ExecutionPayload, ExecutionPayloadBodiesV1,
ExecutionPayloadBodiesV2, ExecutionPayloadInputV2, ExecutionPayloadV1, ExecutionPayloadV3,
ExecutionPayloadV4, ForkchoiceState, ForkchoiceUpdated, PayloadId, PayloadStatus,
TransitionConfiguration,
},
BlobAndProofV1,
};
use reth_rpc_types_compat::engine::payload::{
convert_payload_input_v2_to_payload, convert_to_payload_body_v1, convert_to_payload_body_v2,
};
use reth_storage_api::{BlockReader, HeaderProvider, StateProviderFactory};
use reth_tasks::TaskSpawner;
use reth_transaction_pool::TransactionPool;
use std::{sync::Arc, time::Instant};
use tokio::sync::oneshot;
use tracing::{trace, warn};
@ -37,13 +41,16 @@ pub type EngineApiSender<Ok> = oneshot::Sender<EngineApiResult<Ok>>;
/// The upper limit for payload bodies request.
const MAX_PAYLOAD_BODIES_LIMIT: u64 = 1024;
/// The upper limit blobs `eth_getBlobs`.
const MAX_BLOB_LIMIT: usize = 128;
/// The Engine API implementation that grants the Consensus layer access to data and
/// functions in the Execution layer that are crucial for the consensus process.
pub struct EngineApi<Provider, EngineT: EngineTypes> {
inner: Arc<EngineApiInner<Provider, EngineT>>,
pub struct EngineApi<Provider, EngineT: EngineTypes, Pool> {
inner: Arc<EngineApiInner<Provider, EngineT, Pool>>,
}
struct EngineApiInner<Provider, EngineT: EngineTypes> {
struct EngineApiInner<Provider, EngineT: EngineTypes, Pool> {
/// The provider to interact with the chain.
provider: Provider,
/// Consensus configuration
@ -60,19 +67,24 @@ struct EngineApiInner<Provider, EngineT: EngineTypes> {
client: ClientVersionV1,
/// The list of all supported Engine capabilities available over the engine endpoint.
capabilities: EngineCapabilities,
/// Transaction pool.
tx_pool: Pool,
}
impl<Provider, EngineT> EngineApi<Provider, EngineT>
impl<Provider, EngineT, Pool> EngineApi<Provider, EngineT, Pool>
where
Provider: HeaderProvider + BlockReader + StateProviderFactory + EvmEnvProvider + 'static,
EngineT: EngineTypes,
Pool: TransactionPool + 'static,
{
/// Create new instance of [`EngineApi`].
#[allow(clippy::too_many_arguments)]
pub fn new(
provider: Provider,
chain_spec: Arc<ChainSpec>,
beacon_consensus: BeaconConsensusEngineHandle<EngineT>,
payload_store: PayloadStore<EngineT>,
tx_pool: Pool,
task_spawner: Box<dyn TaskSpawner>,
client: ClientVersionV1,
capabilities: EngineCapabilities,
@ -86,6 +98,7 @@ where
metrics: EngineApiMetrics::default(),
client,
capabilities,
tx_pool,
});
Self { inner }
}
@ -609,10 +622,11 @@ where
}
#[async_trait]
impl<Provider, EngineT> EngineApiServer<EngineT> for EngineApi<Provider, EngineT>
impl<Provider, EngineT, Pool> EngineApiServer<EngineT> for EngineApi<Provider, EngineT, Pool>
where
Provider: HeaderProvider + BlockReader + StateProviderFactory + EvmEnvProvider + 'static,
EngineT: EngineTypes,
Pool: TransactionPool + 'static,
{
/// Handler for `engine_newPayloadV1`
/// See also <https://github.com/ethereum/execution-apis/blob/3d627c95a4d3510a8187dd02e0250ecb4331d27e/src/engine/paris.md#engine_newpayloadv1>
@ -904,9 +918,25 @@ where
async fn exchange_capabilities(&self, _capabilities: Vec<String>) -> RpcResult<Vec<String>> {
Ok(self.inner.capabilities.list())
}
async fn get_blobs_v1(
&self,
versioned_hashes: Vec<B256>,
) -> RpcResult<Vec<Option<BlobAndProofV1>>> {
trace!(target: "rpc::engine", "Serving engine_getBlobsV1");
if versioned_hashes.len() > MAX_BLOB_LIMIT {
return Err(EngineApiError::BlobRequestTooLarge { len: versioned_hashes.len() }.into())
}
Ok(self
.inner
.tx_pool
.get_blobs_for_versioned_hashes(&versioned_hashes)
.map_err(|err| EngineApiError::Internal(Box::new(err)))?)
}
}
impl<Provider, EngineT> std::fmt::Debug for EngineApi<Provider, EngineT>
impl<Provider, EngineT, Pool> std::fmt::Debug for EngineApi<Provider, EngineT, Pool>
where
EngineT: EngineTypes,
{
@ -920,20 +950,21 @@ mod tests {
use super::*;
use assert_matches::assert_matches;
use reth_beacon_consensus::{BeaconConsensusEngineEvent, BeaconEngineMessage};
use reth_ethereum_engine_primitives::EthEngineTypes;
use reth_testing_utils::generators::random_block;
use reth_chainspec::MAINNET;
use reth_ethereum_engine_primitives::EthEngineTypes;
use reth_payload_builder::test_utils::spawn_test_payload_service;
use reth_primitives::SealedBlock;
use reth_provider::test_utils::MockEthProvider;
use reth_rpc_types::engine::{ClientCode, ClientVersionV1};
use reth_rpc_types_compat::engine::payload::execution_payload_from_sealed_block;
use reth_tasks::TokioTaskExecutor;
use reth_testing_utils::generators::random_block;
use reth_tokio_util::EventSender;
use reth_transaction_pool::noop::NoopTransactionPool;
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver};
fn setup_engine_api() -> (EngineApiTestHandle, EngineApi<Arc<MockEthProvider>, EthEngineTypes>)
fn setup_engine_api(
) -> (EngineApiTestHandle, EngineApi<Arc<MockEthProvider>, EthEngineTypes, NoopTransactionPool>)
{
let client = ClientVersionV1 {
code: ClientCode::RH,
@ -953,6 +984,7 @@ mod tests {
chain_spec.clone(),
BeaconConsensusEngineHandle::new(to_engine, event_sender),
payload_store.into(),
NoopTransactionPool::default(),
task_executor,
client,
EngineCapabilities::default(),

View File

@ -42,6 +42,12 @@ pub enum EngineApiError {
/// The length that was requested.
len: u64,
},
/// Too many requested versioned hashes for blobs request
#[error("requested blob count too large: {len}")]
BlobRequestTooLarge {
/// The length that was requested.
len: usize,
},
/// Thrown if `engine_getPayloadBodiesByRangeV1` contains an invalid range
#[error("invalid start ({start}) or count ({count})")]
InvalidBodiesRange {
@ -145,7 +151,8 @@ impl From<EngineApiError> for jsonrpsee_types::error::ErrorObject<'static> {
error.to_string(),
None::<()>,
),
EngineApiError::PayloadRequestTooLarge { .. } => {
EngineApiError::PayloadRequestTooLarge { .. } |
EngineApiError::BlobRequestTooLarge { .. } => {
jsonrpsee_types::error::ErrorObject::owned(
REQUEST_TOO_LARGE_CODE,
REQUEST_TOO_LARGE_MESSAGE,

View File

@ -14,6 +14,7 @@ workspace = true
[dependencies]
# ethereum
alloy-eips.workspace = true
alloy-primitives = { workspace = true, features = ["rand", "rlp", "serde"] }
alloy-rpc-types.workspace = true
alloy-rpc-types-admin.workspace = true
@ -32,6 +33,7 @@ op-alloy-rpc-types-engine.workspace = true
# misc
jsonrpsee-types = { workspace = true, optional = true }
serde.workspace = true
[dev-dependencies]
# misc

View File

@ -61,6 +61,18 @@ pub use eth::{
},
};
use alloy_eips::eip4844::{Blob, Bytes48};
use serde::{Deserialize, Serialize};
/// Blob type returned in responses to `engine_getBlobsV1`: <https://github.com/ethereum/execution-apis/pull/559>
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BlobAndProofV1 {
/// The blob data.
pub blob: Box<Blob>,
/// The KZG proof for the blob.
pub proof: Bytes48,
}
/// Optimism specific rpc types.
pub mod optimism {
pub use op_alloy_rpc_types::*;

View File

@ -18,6 +18,7 @@ reth-chainspec.workspace = true
reth-eth-wire-types.workspace = true
reth-primitives = { workspace = true, features = ["c-kzg", "secp256k1"] }
reth-execution-types.workspace = true
reth-rpc-types.workspace = true
reth-fs-util.workspace = true
reth-storage-api.workspace = true
reth-tasks.workspace = true

View File

@ -5,6 +5,7 @@ use alloy_primitives::{TxHash, B256};
use alloy_rlp::{Decodable, Encodable};
use parking_lot::{Mutex, RwLock};
use reth_primitives::BlobTransactionSidecar;
use reth_rpc_types::BlobAndProofV1;
use schnellru::{ByLength, LruMap};
use std::{collections::HashSet, fmt, fs, io, path::PathBuf, sync::Arc};
use tracing::{debug, trace};
@ -128,6 +129,31 @@ impl BlobStore for DiskFileBlobStore {
self.inner.get_exact(txs)
}
fn get_by_versioned_hashes(
&self,
versioned_hashes: &[B256],
) -> Result<Vec<Option<BlobAndProofV1>>, BlobStoreError> {
let mut result = vec![None; versioned_hashes.len()];
for (_tx_hash, blob_sidecar) in self.inner.blob_cache.lock().iter() {
for (i, blob_versioned_hash) in blob_sidecar.versioned_hashes().enumerate() {
for (j, target_versioned_hash) in versioned_hashes.iter().enumerate() {
if blob_versioned_hash == *target_versioned_hash {
result[j].get_or_insert_with(|| BlobAndProofV1 {
blob: Box::new(blob_sidecar.blobs[i]),
proof: blob_sidecar.proofs[i],
});
}
}
}
// Return early if all blobs are found.
if result.iter().all(|blob| blob.is_some()) {
break;
}
}
Ok(result)
}
fn data_size_hint(&self) -> Option<usize> {
Some(self.inner.size_tracker.data_size())
}

View File

@ -3,6 +3,7 @@ use crate::blobstore::{
};
use alloy_primitives::B256;
use parking_lot::RwLock;
use reth_rpc_types::BlobAndProofV1;
use std::{collections::HashMap, sync::Arc};
/// An in-memory blob store.
@ -113,6 +114,31 @@ impl BlobStore for InMemoryBlobStore {
Ok(items)
}
fn get_by_versioned_hashes(
&self,
versioned_hashes: &[B256],
) -> Result<Vec<Option<BlobAndProofV1>>, BlobStoreError> {
let mut result = vec![None; versioned_hashes.len()];
for (_tx_hash, blob_sidecar) in self.inner.store.read().iter() {
for (i, blob_versioned_hash) in blob_sidecar.versioned_hashes().enumerate() {
for (j, target_versioned_hash) in versioned_hashes.iter().enumerate() {
if blob_versioned_hash == *target_versioned_hash {
result[j].get_or_insert_with(|| BlobAndProofV1 {
blob: Box::new(blob_sidecar.blobs[i]),
proof: blob_sidecar.proofs[i],
});
}
}
}
// Return early if all blobs are found.
if result.iter().all(|blob| blob.is_some()) {
break;
}
}
Ok(result)
}
fn data_size_hint(&self) -> Option<usize> {
Some(self.inner.size_tracker.data_size())
}

View File

@ -5,6 +5,7 @@ pub use disk::{DiskFileBlobStore, DiskFileBlobStoreConfig, OpenDiskFileBlobStore
pub use mem::InMemoryBlobStore;
pub use noop::NoopBlobStore;
use reth_primitives::BlobTransactionSidecar;
use reth_rpc_types::BlobAndProofV1;
use std::{
fmt,
sync::atomic::{AtomicUsize, Ordering},
@ -65,6 +66,12 @@ pub trait BlobStore: fmt::Debug + Send + Sync + 'static {
/// Returns an error if any of the blobs are not found in the blob store.
fn get_exact(&self, txs: Vec<B256>) -> Result<Vec<BlobTransactionSidecar>, BlobStoreError>;
/// Return the [`BlobTransactionSidecar`]s for a list of blob versioned hashes.
fn get_by_versioned_hashes(
&self,
versioned_hashes: &[B256],
) -> Result<Vec<Option<BlobAndProofV1>>, BlobStoreError>;
/// Data size of all transactions in the blob store.
fn data_size_hint(&self) -> Option<usize>;

View File

@ -1,5 +1,6 @@
use crate::blobstore::{BlobStore, BlobStoreCleanupStat, BlobStoreError, BlobTransactionSidecar};
use alloy_primitives::B256;
use reth_rpc_types::BlobAndProofV1;
/// A blobstore implementation that does nothing
#[derive(Clone, Copy, Debug, PartialOrd, PartialEq, Eq, Default)]
@ -49,6 +50,13 @@ impl BlobStore for NoopBlobStore {
Err(BlobStoreError::MissingSidecar(txs[0]))
}
fn get_by_versioned_hashes(
&self,
versioned_hashes: &[B256],
) -> Result<Vec<Option<BlobAndProofV1>>, BlobStoreError> {
Ok(vec![None; versioned_hashes.len()])
}
fn data_size_hint(&self) -> Option<usize> {
Some(0)
}

View File

@ -151,11 +151,12 @@
#![cfg_attr(not(test), warn(unused_crate_dependencies))]
use crate::{identifier::TransactionId, pool::PoolInner};
use alloy_primitives::{Address, TxHash, U256};
use alloy_primitives::{Address, TxHash, B256, U256};
use aquamarine as _;
use reth_eth_wire_types::HandleMempoolData;
use reth_execution_types::ChangedAccount;
use reth_primitives::{BlobTransactionSidecar, PooledTransactionsElement};
use reth_rpc_types::BlobAndProofV1;
use reth_storage_api::StateProviderFactory;
use std::{collections::HashSet, sync::Arc};
use tokio::sync::mpsc::Receiver;
@ -526,6 +527,13 @@ where
self.pool.blob_store().get_exact(tx_hashes)
}
fn get_blobs_for_versioned_hashes(
&self,
versioned_hashes: &[B256],
) -> Result<Vec<Option<BlobAndProofV1>>, BlobStoreError> {
self.pool.blob_store().get_by_versioned_hashes(versioned_hashes)
}
/// Returns all pending transactions filtered by [`TransactionOrigin`]
fn get_pending_transactions_by_origin(
&self,

View File

@ -16,9 +16,10 @@ use crate::{
PooledTransactionsElement, PropagatedTransactions, TransactionEvents, TransactionOrigin,
TransactionPool, TransactionValidationOutcome, TransactionValidator, ValidPoolTransaction,
};
use alloy_primitives::{Address, TxHash, U256};
use alloy_primitives::{Address, TxHash, B256, U256};
use reth_eth_wire_types::HandleMempoolData;
use reth_primitives::BlobTransactionSidecar;
use reth_rpc_types::BlobAndProofV1;
use std::{collections::HashSet, marker::PhantomData, sync::Arc};
use tokio::sync::{mpsc, mpsc::Receiver};
@ -244,6 +245,13 @@ impl TransactionPool for NoopTransactionPool {
Err(BlobStoreError::MissingSidecar(tx_hashes[0]))
}
fn get_blobs_for_versioned_hashes(
&self,
versioned_hashes: &[B256],
) -> Result<Vec<Option<BlobAndProofV1>>, BlobStoreError> {
Ok(vec![None; versioned_hashes.len()])
}
fn get_pending_transactions_by_origin(
&self,
_origin: TransactionOrigin,

View File

@ -17,6 +17,7 @@ use reth_primitives::{
PooledTransactionsElementEcRecovered, SealedBlock, Transaction, TransactionSignedEcRecovered,
EIP1559_TX_TYPE_ID, EIP4844_TX_TYPE_ID, EIP7702_TX_TYPE_ID,
};
use reth_rpc_types::BlobAndProofV1;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use std::{
@ -414,6 +415,12 @@ pub trait TransactionPool: Send + Sync + Clone {
&self,
tx_hashes: Vec<TxHash>,
) -> Result<Vec<BlobTransactionSidecar>, BlobStoreError>;
/// Return the [`BlobTransactionSidecar`]s for a list of blob versioned hashes.
fn get_blobs_for_versioned_hashes(
&self,
versioned_hashes: &[B256],
) -> Result<Vec<Option<BlobAndProofV1>>, BlobStoreError>;
}
/// Extension for [TransactionPool] trait that allows to set the current block info.