Skip to content

Commit

Permalink
feat: add debug_getRawBlock (kkrt-labs#877)
Browse files Browse the repository at this point in the history
* feat: add debug_getRawBlock

* rebase and prepare first test

* feat: add much needed tests

* answer review
  • Loading branch information
Eikix authored Mar 22, 2024
1 parent 929e18c commit 59a9521
Show file tree
Hide file tree
Showing 7 changed files with 295 additions and 12 deletions.
2 changes: 1 addition & 1 deletion src/eth_provider/error.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use jsonrpsee::types::ErrorObject;
use thiserror::Error;

use crate::models::felt::ConversionError;
use crate::models::ConversionError;

/// List of JSON-RPC error codes from ETH rpc spec.
/// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1474.md
Expand Down
15 changes: 13 additions & 2 deletions src/eth_rpc/servers/debug_rpc.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use crate::eth_provider::error::{EthApiError, ReceiptError, SignatureError};
use crate::eth_rpc::api::debug_api::DebugApiServer;
use crate::models::block::rpc_to_primitive_block;
use crate::{eth_provider::provider::EthereumProvider, models::transaction::rpc_transaction_to_primitive};
use alloy_rlp::Encodable;
use jsonrpsee::core::{async_trait, RpcResult as Result};
use reth_primitives::{Bytes, Log, Receipt, ReceiptWithBloom, TransactionSigned, B256};
use reth_rpc_types::BlockId;
Expand All @@ -24,8 +26,17 @@ impl<P: EthereumProvider + Send + Sync + 'static> DebugApiServer for DebugRpc<P>
}

/// Returns an RLP-encoded block.
async fn raw_block(&self, _block_id: BlockId) -> Result<Bytes> {
Err(EthApiError::Unsupported("debug_rawBlock").into())
async fn raw_block(&self, block_id: BlockId) -> Result<Bytes> {
let block = match block_id {
BlockId::Hash(hash) => self.eth_provider.block_by_hash(hash.into(), true).await?,
BlockId::Number(number) => self.eth_provider.block_by_number(number, true).await?,
};
let mut raw_block = Vec::new();
if let Some(block) = block {
let block = rpc_to_primitive_block(block.inner).map_err(EthApiError::from)?;
block.encode(&mut raw_block);
}
Ok(Bytes::from(raw_block))
}

/// Returns a EIP-2718 binary-encoded transaction.
Expand Down
207 changes: 204 additions & 3 deletions src/models/block.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use reth_primitives::{BlockId as EthereumBlockId, BlockNumberOrTag};
use reth_primitives::{BlockId as EthereumBlockId, BlockNumberOrTag, TransactionSigned, Withdrawals};
use starknet::core::types::{BlockId as StarknetBlockId, BlockTag};

use super::felt::ConversionError;
use crate::into_via_try_wrapper;
use super::{transaction::rpc_transaction_to_primitive, ConversionError};
use crate::{eth_provider::error::KakarotError, into_via_try_wrapper};

pub struct EthBlockId(EthereumBlockId);

Expand Down Expand Up @@ -60,3 +60,204 @@ impl From<EthBlockNumberOrTag> for StarknetBlockId {
}
}
}

pub fn rpc_to_primitive_block(block: reth_rpc_types::Block) -> Result<reth_primitives::Block, KakarotError> {
let base_fee_per_gas: Option<u64> = block
.header
.base_fee_per_gas
.map(|base_fee_per_gas| base_fee_per_gas.try_into().map_err(|_| ConversionError))
.transpose()?;
let header = reth_primitives::Header {
base_fee_per_gas,
beneficiary: block.header.miner,
blob_gas_used: block.header.blob_gas_used.map(|blob_gas_used| blob_gas_used.to::<u64>()),
difficulty: block.header.difficulty,
excess_blob_gas: block.header.excess_blob_gas.map(|excess_blob_gas| excess_blob_gas.to::<u64>()),
extra_data: block.header.extra_data,
gas_limit: block.header.gas_limit.try_into().map_err(|_| ConversionError)?,
gas_used: block.header.gas_used.try_into().map_err(|_| ConversionError)?,
logs_bloom: block.header.logs_bloom,
mix_hash: block.header.mix_hash.unwrap_or_default(),
nonce: u64::from_be_bytes(block.header.nonce.unwrap_or_default().0),
number: block.header.number.ok_or(ConversionError)?.try_into().map_err(|_| ConversionError)?,
ommers_hash: block.header.uncles_hash,
parent_beacon_block_root: block.header.parent_beacon_block_root,
parent_hash: block.header.parent_hash,
receipts_root: block.header.receipts_root,
state_root: block.header.state_root,
timestamp: block.header.timestamp.try_into().map_err(|_| ConversionError)?,
transactions_root: block.header.transactions_root,
withdrawals_root: block.header.withdrawals_root,
};
let body = {
let transactions: Result<Vec<TransactionSigned>, KakarotError> = match block.transactions {
reth_rpc_types::BlockTransactions::Full(transactions) => transactions
.into_iter()
.map(|tx| {
let signature = tx.signature.ok_or(ConversionError)?;
let tx_signed = TransactionSigned::from_transaction_and_signature(
rpc_transaction_to_primitive(tx)?,
reth_primitives::Signature {
r: signature.r,
s: signature.s,
odd_y_parity: signature.y_parity.unwrap_or(reth_rpc_types::Parity(false)).0,
},
);
Ok(tx_signed)
})
.collect(),
reth_rpc_types::BlockTransactions::Hashes(_transaction_hashes) => {
return Err(ConversionError.into());
}
reth_rpc_types::BlockTransactions::Uncle => {
return Err(ConversionError.into());
}
};
transactions?
};
// ⚠️ Kakarot does not support omners or withdrawals and returns default values for those fields ⚠️
Ok(reth_primitives::Block { header, body, ommers: Default::default(), withdrawals: Some(Withdrawals::default()) })
}

#[cfg(test)]
mod tests {
use std::str::FromStr;

use reth_primitives::{Address, Bloom, Bytes, B256, B64, U128, U256, U64};
use reth_rpc_types::{other::OtherFields, Parity, Signature};

use super::*;

fn base_rpc_header() -> reth_rpc_types::Header {
reth_rpc_types::Header {
parent_hash: B256::from_str(&format!("0x{:0>64}", "01")).unwrap(),
uncles_hash: B256::from_str(&format!("0x{:0>64}", "02")).unwrap(),
miner: Address::from_str(&format!("0x{:0>40}", "03")).unwrap(),
state_root: B256::from_str(&format!("0x{:0>64}", "04")).unwrap(),
transactions_root: B256::from_str(&format!("0x{:0>64}", "05")).unwrap(),
receipts_root: B256::from_str(&format!("0x{:0>64}", "06")).unwrap(),
withdrawals_root: Some(B256::from_str(&format!("0x{:0>64}", "07")).unwrap()),
logs_bloom: Bloom::ZERO,
difficulty: U256::ZERO,
base_fee_per_gas: Some(U256::from(8)),
blob_gas_used: Some(U64::from(9)),
excess_blob_gas: Some(U64::from(10)),
extra_data: Bytes::default(),
gas_limit: U256::from(11),
gas_used: U256::from(12),
hash: Some(B256::from_str(&format!("0x{:0>64}", "D")).unwrap()),
mix_hash: Some(B256::from_str(&format!("0x{:0>64}", "E")).unwrap()),
parent_beacon_block_root: Some(B256::from_str(&format!("0x{:0>64}", "F")).unwrap()),
nonce: Some(B64::from_str(&format!("0x{:0>16}", "10")).unwrap()),
number: Some(U256::from(17)),
timestamp: U256::from(18),
total_difficulty: None,
}
}

fn base_rpc_transaction() -> reth_rpc_types::Transaction {
reth_rpc_types::Transaction {
hash: B256::default(),
nonce: U64::from(1),
block_hash: None,
block_number: None,
transaction_index: Some(U256::ZERO),
from: Address::from_str("0x0000000000000000000000000000000000000001").unwrap(),
to: Some(Address::from_str("0x0000000000000000000000000000000000000002").unwrap()),
value: U256::from(100),
gas_price: Some(U128::from(20)),
gas: U256::from(21000),
max_fee_per_gas: Some(U128::from(30)),
max_priority_fee_per_gas: Some(U128::from(10)),
max_fee_per_blob_gas: None,
input: Bytes::from("1234"),
signature: Some(Signature {
r: U256::from(111),
s: U256::from(222),
v: U256::from(1),
y_parity: Some(Parity(true)),
}),
chain_id: Some(U64::from(1)),
blob_versioned_hashes: vec![],
access_list: None,
transaction_type: Some(U64::from(2)),
other: serde_json::from_str("{}").unwrap(),
}
}

fn base_rpc_block() -> reth_rpc_types::Block {
reth_rpc_types::Block {
header: base_rpc_header(),
uncles: Vec::default(),
transactions: reth_rpc_types::BlockTransactions::Full(vec![
base_rpc_transaction(),
base_rpc_transaction(),
base_rpc_transaction(),
]),
size: None,
withdrawals: Some(Vec::default()),
other: OtherFields::default(),
}
}

#[test]
fn test_rpc_to_primitive_block() {
let block = base_rpc_block();
let primitive_block = rpc_to_primitive_block(block).unwrap();
assert_eq!(primitive_block.header.parent_hash, B256::from_str(&format!("0x{:0>64}", "01")).unwrap());
assert_eq!(primitive_block.header.ommers_hash, B256::from_str(&format!("0x{:0>64}", "02")).unwrap());
assert_eq!(primitive_block.header.beneficiary, Address::from_str(&format!("0x{:0>40}", "03")).unwrap());
assert_eq!(primitive_block.header.state_root, B256::from_str(&format!("0x{:0>64}", "04")).unwrap());
assert_eq!(primitive_block.header.transactions_root, B256::from_str(&format!("0x{:0>64}", "05")).unwrap());
assert_eq!(primitive_block.header.receipts_root, B256::from_str(&format!("0x{:0>64}", "06")).unwrap());
assert_eq!(
primitive_block.header.withdrawals_root.unwrap(),
B256::from_str(&format!("0x{:0>64}", "07")).unwrap()
);
assert_eq!(primitive_block.header.logs_bloom, Bloom::ZERO);
assert_eq!(primitive_block.header.difficulty, U256::ZERO);
assert_eq!(primitive_block.header.base_fee_per_gas, Some(8));
assert_eq!(primitive_block.header.blob_gas_used, Some(9u64));
assert_eq!(primitive_block.header.excess_blob_gas, Some(10u64));
assert_eq!(primitive_block.header.gas_limit, 11u64);
assert_eq!(primitive_block.header.gas_used, 12u64);
assert_eq!(primitive_block.header.mix_hash, B256::from_str(&format!("0x{:0>64}", "E")).unwrap());
assert_eq!(
primitive_block.header.nonce,
u64::from_be_bytes(B64::from_str(&format!("0x{:0>16}", "10")).unwrap().0)
);
assert_eq!(primitive_block.header.number, 17u64);
assert_eq!(primitive_block.header.timestamp, 18u64);
assert_eq!(
primitive_block.body,
vec![
TransactionSigned::from_transaction_and_signature(
rpc_transaction_to_primitive(base_rpc_transaction()).unwrap(),
reth_primitives::Signature {
r: base_rpc_transaction().signature.unwrap().r,
s: base_rpc_transaction().signature.unwrap().s,
odd_y_parity: base_rpc_transaction().signature.unwrap().y_parity.unwrap().0,
},
),
TransactionSigned::from_transaction_and_signature(
rpc_transaction_to_primitive(base_rpc_transaction()).unwrap(),
reth_primitives::Signature {
r: base_rpc_transaction().signature.unwrap().r,
s: base_rpc_transaction().signature.unwrap().s,
odd_y_parity: base_rpc_transaction().signature.unwrap().y_parity.unwrap().0,
},
),
TransactionSigned::from_transaction_and_signature(
rpc_transaction_to_primitive(base_rpc_transaction()).unwrap(),
reth_primitives::Signature {
r: base_rpc_transaction().signature.unwrap().r,
s: base_rpc_transaction().signature.unwrap().s,
odd_y_parity: base_rpc_transaction().signature.unwrap().y_parity.unwrap().0,
},
)
]
);
assert_eq!(primitive_block.withdrawals, Some(Withdrawals::default()));
assert_eq!(primitive_block.ommers, Vec::default());
}
}
8 changes: 3 additions & 5 deletions src/models/felt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ use reth_primitives::{Address, B256, U256, U64};
use starknet::core::types::{EthAddress, FieldElement};
use std::ops::{Deref, DerefMut};

#[derive(Debug, thiserror::Error)]
#[error("conversion failed")]
pub struct ConversionError;
use super::ConversionError;

#[derive(Clone, Debug)]
pub struct Felt252Wrapper(FieldElement);
Expand Down Expand Up @@ -98,9 +96,9 @@ macro_rules! into_via_wrapper {
#[macro_export]
macro_rules! into_via_try_wrapper {
($val: expr) => {{
let intermediate: Result<_, $crate::models::felt::ConversionError> =
let intermediate: Result<_, $crate::models::ConversionError> =
TryInto::<$crate::models::felt::Felt252Wrapper>::try_into($val)
.map_err(|_| $crate::models::felt::ConversionError)
.map_err(|_| $crate::models::ConversionError)
.map(Into::into);
intermediate
}};
Expand Down
4 changes: 4 additions & 0 deletions src/models/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@ pub mod balance;
pub mod block;
pub mod felt;
pub mod transaction;

#[derive(Debug, thiserror::Error)]
#[error("conversion failed")]
pub struct ConversionError;
2 changes: 1 addition & 1 deletion src/models/transaction.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use reth_primitives::{AccessList, AccessListItem, TransactionKind, TxEip1559, TxEip2930, TxLegacy, TxType};

use super::felt::ConversionError;
use super::ConversionError;
use crate::eth_provider::error::KakarotError;

pub fn rpc_transaction_to_primitive(
Expand Down
69 changes: 69 additions & 0 deletions tests/debug_api.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#![cfg(feature = "testing")]
use alloy_rlp::{Decodable, Encodable};
use kakarot_rpc::eth_provider::provider::EthereumProvider;
use kakarot_rpc::models::block::rpc_to_primitive_block;
use kakarot_rpc::test_utils::fixtures::{katana, setup};
use kakarot_rpc::test_utils::katana::Katana;
use kakarot_rpc::test_utils::mongo::{BLOCK_HASH, BLOCK_NUMBER, EIP1599_TX_HASH, EIP2930_TX_HASH, LEGACY_TX_HASH};
Expand Down Expand Up @@ -290,3 +292,70 @@ async fn test_raw_receipts(#[future] katana: Katana, _setup: ()) {
// Stop the Kakarot RPC server.
drop(server_handle);
}

#[rstest]
#[awt]
#[tokio::test(flavor = "multi_thread")]
async fn test_raw_block(#[future] katana: Katana, _setup: ()) {
let (server_addr, server_handle) =
start_kakarot_rpc_server(&katana).await.expect("Error setting up Kakarot RPC server");

let reqwest_client = reqwest::Client::new();
let res = reqwest_client
.post(format!("http://localhost:{}", server_addr.port()))
.header("Content-Type", "application/json")
.body(
json!(
{
"jsonrpc":"2.0",
"method":"debug_getRawBlock",
"params":[format!("0x{:064x}", BLOCK_NUMBER)],
"id":1,
}
)
.to_string(),
)
.send()
.await
.expect("Failed to call Debug RPC");
let response = res.text().await.expect("Failed to get response body");
let raw: Value = serde_json::from_str(&response).expect("Failed to deserialize response body");
let rlp_bytes: Option<Bytes> = serde_json::from_value(raw["result"].clone()).expect("Failed to deserialize result");
assert!(rlp_bytes.is_some());

// Query the block with eth_getBlockByNumber
let res = reqwest_client
.post(format!("http://localhost:{}", server_addr.port()))
.header("Content-Type", "application/json")
.body(
json!(
{
"jsonrpc":"2.0",
"method":"eth_getBlockByNumber",
"params":[format!("0x{:x}", BLOCK_NUMBER), true],
"id":1,
}
)
.to_string(),
)
.send()
.await
.expect("Failed to call Debug RPC");
let response = res.text().await.expect("Failed to get response body");
let response: Value = serde_json::from_str(&response).expect("Failed to deserialize response body");
let rpc_block: reth_rpc_types::Block =
serde_json::from_value(response["result"].clone()).expect("Failed to deserialize result");
let primitive_block = rpc_to_primitive_block(rpc_block).unwrap();

// Encode primitive block and compare with the result of debug_getRawBlock
let mut buf = Vec::new();
primitive_block.encode(&mut buf);
assert_eq!(rlp_bytes.clone().unwrap(), Bytes::from(buf));

// Decode encoded block and compare with the block from eth_getBlockByNumber
let decoded_block = reth_primitives::Block::decode(&mut rlp_bytes.unwrap().as_ref()).unwrap();
assert_eq!(decoded_block, primitive_block);

// Stop the Kakarot RPC server.
drop(server_handle);
}

0 comments on commit 59a9521

Please sign in to comment.