Skip to content

Commit

Permalink
feat: add raw transactions endpoint (kkrt-labs#897)
Browse files Browse the repository at this point in the history
* add raw transactions endpoint

* fix comments
  • Loading branch information
greged93 authored Mar 27, 2024
1 parent 95dc526 commit 0ba8694
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 19 deletions.
2 changes: 1 addition & 1 deletion .trunk/trunk.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ lint:
- [email protected]
- [email protected]
- [email protected]
- [email protected].2
- [email protected].3
- [email protected]
ignore:
- linters: [ALL]
Expand Down
5 changes: 5 additions & 0 deletions src/eth_provider/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,13 +189,18 @@ pub enum TransactionError {
/// Thrown when the gas used overflows u128.
#[error("gas overflow")]
GasOverflow,
/// Thrown when the transaction isn't the
/// BlockTransactions::FullTransactions variant.
#[error("expected full transactions")]
ExpectedFullTransactions,
}

impl TransactionError {
pub fn error_code(&self) -> EthRpcErrorCode {
match self {
TransactionError::InvalidChainId => EthRpcErrorCode::InvalidInput,
TransactionError::GasOverflow => EthRpcErrorCode::TransactionRejected,
TransactionError::ExpectedFullTransactions => EthRpcErrorCode::InternalError,
}
}
}
Expand Down
62 changes: 52 additions & 10 deletions src/eth_provider/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,15 @@ pub trait EthereumProvider {
) -> EthProviderResult<FeeHistory>;
/// Send a raw transaction to the network and returns the transactions hash.
async fn send_raw_transaction(&self, transaction: Bytes) -> EthProviderResult<B256>;
/// Returns the current gas price.
async fn gas_price(&self) -> EthProviderResult<U256>;
/// Returns the block receipts for a block.
async fn block_receipts(&self, block_id: Option<BlockId>) -> EthProviderResult<Option<Vec<TransactionReceipt>>>;
/// Returns the transactions for a block.
async fn block_transactions(
&self,
block_id: Option<BlockId>,
) -> EthProviderResult<Option<Vec<reth_rpc_types::Transaction>>>;
}

/// Structure that implements the EthereumProvider trait.
Expand Down Expand Up @@ -597,6 +604,30 @@ where
}
}
}

async fn block_transactions(
&self,
block_id: Option<BlockId>,
) -> EthProviderResult<Option<Vec<reth_rpc_types::Transaction>>> {
let block_id = block_id.unwrap_or(BlockId::Number(BlockNumberOrTag::Latest));
let block_id = match block_id {
BlockId::Number(maybe_number) => {
BlockHashOrNumber::Number(self.tag_into_block_number(maybe_number).await?.to())
}
BlockId::Hash(hash) => BlockHashOrNumber::Hash(hash.block_hash),
};

let block_exists = self.block_exists(block_id).await?;
if !block_exists {
return Ok(None);
}

let transactions = self.transactions(block_id, true).await?;
match transactions {
BlockTransactions::Full(transactions) => Ok(Some(transactions)),
_ => Err(TransactionError::ExpectedFullTransactions.into()),
}
}
}

impl<SP> EthDataProvider<SP>
Expand Down Expand Up @@ -710,21 +741,18 @@ where
.map_err(|_| EthApiError::UnknownBlock)
}

/// Get a block from the database based on a block hash or number.
/// If full is true, the block will contain the full transactions, otherwise just the hashes
async fn block(&self, block_id: BlockHashOrNumber, full: bool) -> EthProviderResult<Option<RichBlock>> {
let header = self.header(block_id).await?;
let header = match header {
Some(header) => header,
None => return Ok(None),
};

/// Return the transactions given a block id.
pub(crate) async fn transactions(
&self,
block_id: BlockHashOrNumber,
full: bool,
) -> EthProviderResult<BlockTransactions> {
let transactions_filter = match block_id {
BlockHashOrNumber::Hash(hash) => into_filter("tx.blockHash", hash, 64),
BlockHashOrNumber::Number(number) => into_filter("tx.blockNumber", number, 64),
};

let transactions = if full {
let block_transactions = if full {
BlockTransactions::Full(iter_into(
self.database.get::<StoredTransaction>("transactions", transactions_filter, None).await?,
))
Expand All @@ -736,6 +764,20 @@ where
))
};

Ok(block_transactions)
}

/// Get a block from the database based on a block hash or number.
/// If full is true, the block will contain the full transactions, otherwise just the hashes
async fn block(&self, block_id: BlockHashOrNumber, full: bool) -> EthProviderResult<Option<RichBlock>> {
let header = self.header(block_id).await?;
let header = match header {
Some(header) => header,
None => return Ok(None),
};

let transactions = self.transactions(block_id, full).await?;

// The withdrawals are not supported, hence the withdrawals_root should always be empty.
let withdrawal_root = header.header.withdrawals_root.unwrap_or_default();
if withdrawal_root != EMPTY_ROOT_HASH {
Expand Down
36 changes: 28 additions & 8 deletions src/eth_rpc/servers/debug_rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,36 +46,56 @@ impl<P: EthereumProvider + Send + Sync + 'static> DebugApiServer for DebugRpc<P>
let transaction = self.eth_provider.transaction_by_hash(hash).await?;

if let Some(tx) = transaction {
let mut raw_transaction = Vec::new();
let signature = tx.signature.ok_or_else(|| EthApiError::from(SignatureError::MissingSignature))?;
let tx = rpc_transaction_to_primitive(tx).map_err(EthApiError::from)?;
TransactionSigned::from_transaction_and_signature(
let bytes = TransactionSigned::from_transaction_and_signature(
tx,
reth_primitives::Signature {
r: signature.r,
s: signature.s,
odd_y_parity: signature.y_parity.unwrap_or(reth_rpc_types::Parity(false)).0,
},
)
.encode_enveloped(&mut raw_transaction);
Ok(Some(Bytes::from(raw_transaction)))
.envelope_encoded();
Ok(Some(bytes))
} else {
Ok(None)
}
}

/// Returns an array of EIP-2718 binary-encoded transactions for the given [BlockId].
async fn raw_transactions(&self, _block_id: BlockId) -> Result<Vec<Bytes>> {
Err(EthApiError::Unsupported("debug_rawTransactions").into())
async fn raw_transactions(&self, block_id: BlockId) -> Result<Vec<Bytes>> {
let transactions = self.eth_provider.block_transactions(Some(block_id)).await?.unwrap_or_default();

let mut raw_transactions = Vec::with_capacity(transactions.len());

for t in transactions {
let signature = t.signature.ok_or_else(|| EthApiError::from(SignatureError::MissingSignature))?;
let tx = rpc_transaction_to_primitive(t).map_err(EthApiError::from)?;
let bytes = TransactionSigned::from_transaction_and_signature(
tx,
reth_primitives::Signature {
r: signature.r,
s: signature.s,
odd_y_parity: signature.y_parity.unwrap_or(reth_rpc_types::Parity(false)).0,
},
)
.envelope_encoded();
raw_transactions.push(bytes);
}

Ok(raw_transactions)
}

/// Returns an array of EIP-2718 binary-encoded receipts.
async fn raw_receipts(&self, block_id: BlockId) -> Result<Vec<Bytes>> {
let receipts = self.eth_provider.block_receipts(Some(block_id)).await?.unwrap_or_default();

// Initializes an empty vector to store the raw receipts
let mut raw_receipts = Vec::new();
let mut raw_receipts = Vec::with_capacity(receipts.len());

// Iterates through the receipts of the block using the `block_receipts` method of the Ethereum API
for receipt in self.eth_provider.block_receipts(Some(block_id)).await?.unwrap_or_default() {
for receipt in receipts {
// Converts the transaction type to a u8 and then tries to convert it into TxType
let tx_type = match receipt.transaction_type.to::<u8>().try_into() {
Ok(tx_type) => tx_type,
Expand Down
98 changes: 98 additions & 0 deletions tests/debug_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
use alloy_rlp::{Decodable, Encodable};
use kakarot_rpc::eth_provider::provider::EthereumProvider;
use kakarot_rpc::models::block::rpc_to_primitive_block;
use kakarot_rpc::models::transaction::rpc_transaction_to_primitive;
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 @@ -191,6 +192,103 @@ async fn test_raw_transaction(#[future] katana: Katana, _setup: ()) {
drop(server_handle);
}

#[rstest]
#[awt]
#[tokio::test(flavor = "multi_thread")]
/// Test for fetching raw transactions by block hash and block number.
async fn test_raw_transactions(#[future] katana: Katana, _setup: ()) {
// Start the Kakarot RPC server.
let (server_addr, server_handle) =
start_kakarot_rpc_server(&katana).await.expect("Error setting up Kakarot RPC server");

// Fetch raw transactions by block hash.
let reqwest_client = reqwest::Client::new();
let res_by_block_hash = reqwest_client
.post(format!("http://localhost:{}", server_addr.port()))
.header("Content-Type", "application/json")
.body(
json!(
{
"jsonrpc":"2.0",
"method":"debug_getRawTransactions",
"params":[format!("0x{:064x}", *BLOCK_HASH)],
"id":1,
}
)
.to_string(),
)
.send()
.await
.expect("Failed to call Debug RPC");
let response_by_block_hash = res_by_block_hash.text().await.expect("Failed to get response body");
let raw_by_block_hash: Value =
serde_json::from_str(&response_by_block_hash).expect("Failed to deserialize response body");

let rlp_bytes_by_block_hash: Vec<Bytes> =
serde_json::from_value(raw_by_block_hash["result"].clone()).expect("Failed to deserialize result");

// Fetch raw transactions by block number.
let res_by_block_number = reqwest_client
.post(format!("http://localhost:{}", server_addr.port()))
.header("Content-Type", "application/json")
.body(
json!(
{
"jsonrpc":"2.0",
"method":"debug_getRawTransactions",
"params":[format!("0x{:064x}", BLOCK_NUMBER)],
"id":1,
}
)
.to_string(),
)
.send()
.await
.expect("Failed to call Debug RPC");
let response_by_block_number = res_by_block_number.text().await.expect("Failed to get response body");
let raw_by_block_number: Value =
serde_json::from_str(&response_by_block_number).expect("Failed to deserialize response body");

let rlp_bytes_by_block_number: Vec<Bytes> =
serde_json::from_value(raw_by_block_number["result"].clone()).expect("Failed to deserialize result");

// Assert equality of transactions fetched by block hash and block number.
assert_eq!(rlp_bytes_by_block_number, rlp_bytes_by_block_hash);

let eth_provider = katana.eth_provider();

for (i, actual_tx) in eth_provider
.block_transactions(Some(reth_rpc_types::BlockId::Number(BlockNumberOrTag::Number(BLOCK_NUMBER))))
.await
.unwrap()
.unwrap()
.iter()
.enumerate()
{
// Fetch the transaction for the current transaction hash.
let tx = eth_provider.transaction_by_hash(actual_tx.hash).await.unwrap().unwrap();
let signature = tx.signature.unwrap();

// Convert the transaction to a primitives transactions and encode it.
let rlp_bytes = TransactionSigned::from_transaction_and_signature(
rpc_transaction_to_primitive(tx).unwrap(),
reth_primitives::Signature {
r: signature.r,
s: signature.s,
odd_y_parity: signature.y_parity.unwrap_or(reth_rpc_types::Parity(false)).0,
},
)
.envelope_encoded();

// Assert the equality of the constructed receipt with the corresponding receipt from both block hash and block number.
assert_eq!(rlp_bytes_by_block_number[i], rlp_bytes);
assert_eq!(rlp_bytes_by_block_hash[i], rlp_bytes);
}

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

#[rstest]
#[awt]
#[tokio::test(flavor = "multi_thread")]
Expand Down

0 comments on commit 0ba8694

Please sign in to comment.