Commit 679eb237 authored by clabby's avatar clabby

:broom: dylib

parent e9a8f81a
......@@ -5360,6 +5360,7 @@ dependencies = [
name = "rethdb-reader"
version = "0.1.0"
dependencies = [
"anyhow",
"reth",
"serde",
"serde_json",
......
......@@ -12,3 +12,4 @@ crate-type = ["cdylib"]
reth = { git = "https://github.com/paradigmxyz/reth.git" }
serde = "1.0.190"
serde_json = "1.0.107"
anyhow = "1.0.75"
......@@ -3,37 +3,81 @@
A dylib to be accessed via FFI in `op-service`'s `sources` package for reading information
directly from the `reth` database.
## Developing
**Building**
To build the dylib, you must first have the [Rust Toolchain][rust-toolchain] installed.
```sh
cargo build --release
```
**Docs**
Documentation is available via rustdoc.
```sh
cargo doc --open
```
**Linting**
```sh
cargo +nightly fmt -- && cargo +nightly clippy --all --all-features -- -D warnings
```
**Generating the C header**
To generate the C header, first install `cbindgen` via `cargo install cbindgen --force`. Then, run the generation script:
```sh
./headgen.sh
```
### C Header
The C header below is generated by `cbindgen`, and it is the interface that consumers of the dylib use to call its exported
functions. Currently, the only exported functions pertain to reading fully hydrated block receipts from the database.
```c
#include <cstdarg>
#include <cstdint>
#include <cstdlib>
#include <ostream>
#include <new>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
struct ReceiptsResult {
/**
* A [ReceiptsResult] is a wrapper around a JSON string containing serialized [TransactionReceipt]s
* as well as an error status that is compatible with FFI.
*
* # Safety
* - When the `error` field is false, the `data` pointer is guaranteed to be valid.
* - When the `error` field is true, the `data` pointer is guaranteed to be null.
*/
typedef struct ReceiptsResult {
uint32_t *data;
uintptr_t data_len;
bool error;
};
extern "C" {
} ReceiptsResult;
/// Read the receipts for a blockhash from the RETH database directly.
///
/// # Safety
/// - All possible nil pointer dereferences are checked, and the function will return a
/// failing [ReceiptsResult] if any are found.
ReceiptsResult read_receipts(const uint8_t *block_hash,
/**
* Read the receipts for a blockhash from the RETH database directly.
*
* # Safety
* - All possible nil pointer dereferences are checked, and the function will return a
* failing [ReceiptsResult] if any are found.
*/
struct ReceiptsResult read_receipts(const uint8_t *block_hash,
uintptr_t block_hash_len,
const char *db_path);
/// Free a string that was allocated in Rust and passed to C.
///
/// # Safety
/// - All possible nil pointer dereferences are checked.
/**
* Free a string that was allocated in Rust and passed to C.
*
* # Safety
* - All possible nil pointer dereferences are checked.
*/
void free_string(char *string);
}
```
[rust-toolchain]: https://rustup.rs/
#!/bin/bash
set -e
# Generate rdb.h
cbindgen --crate rethdb-reader --output rdb.h -l C
# Process README.md to replace the content within the specified code block
awk '
BEGIN { in_code_block=0; }
/^```c/ { in_code_block=1; print; next; }
/^```/ && in_code_block { in_code_block=0; while ((getline line < "rdb.h") > 0) print line; }
!in_code_block { print; }
' README.md > README.tmp && mv README.tmp README.md
echo "Generated C header successfully"
#![doc = include_str!("../README.md")]
use reth::{
blockchain_tree::noop::NoopBlockchainTree,
primitives::{
BlockHashOrNumber, Receipt, TransactionKind, TransactionMeta, TransactionSigned, MAINNET,
U128, U256, U64,
},
providers::{providers::BlockchainProvider, BlockReader, ProviderFactory, ReceiptProvider},
rpc::types::{Log, TransactionReceipt},
utils::db::open_db_read_only,
};
use std::{os::raw::c_char, path::Path};
use receipts::{read_receipts_inner, ReceiptsResult};
use std::os::raw::c_char;
/// A [ReceiptsResult] is a wrapper around a JSON string containing serialized [TransactionReceipt]s
/// as well as an error status that is compatible with FFI.
///
/// # Safety
/// - When the `error` field is false, the `data` pointer is guaranteed to be valid.
/// - When the `error` field is true, the `data` pointer is guaranteed to be null.
#[repr(C)]
pub struct ReceiptsResult {
data: *mut char,
data_len: usize,
error: bool,
}
impl ReceiptsResult {
/// Constructs a successful [ReceiptsResult] from a JSON string.
pub fn success(data: *mut char, data_len: usize) -> Self {
Self {
data,
data_len,
error: false,
}
}
/// Constructs a failing [ReceiptsResult] with a null pointer to the data.
pub fn fail() -> Self {
Self {
data: std::ptr::null_mut(),
data_len: 0,
error: true,
}
}
}
mod receipts;
/// Read the receipts for a blockhash from the RETH database directly.
///
......@@ -56,88 +16,7 @@ pub unsafe extern "C" fn read_receipts(
block_hash_len: usize,
db_path: *const c_char,
) -> ReceiptsResult {
// Convert the raw pointer and length back to a Rust slice
let Ok(block_hash): Result<[u8; 32], _> = {
if block_hash.is_null() {
return ReceiptsResult::fail();
}
std::slice::from_raw_parts(block_hash, block_hash_len)
}
.try_into() else {
return ReceiptsResult::fail();
};
// Convert the *const c_char to a Rust &str
let Ok(db_path_str) = {
if db_path.is_null() {
return ReceiptsResult::fail();
}
std::ffi::CStr::from_ptr(db_path)
}
.to_str() else {
return ReceiptsResult::fail();
};
let Ok(db) = open_db_read_only(Path::new(db_path_str), None) else {
return ReceiptsResult::fail();
};
let factory = ProviderFactory::new(db, MAINNET.clone());
// Create a read-only BlockChainProvider
let Ok(provider) = BlockchainProvider::new(factory, NoopBlockchainTree::default()) else {
return ReceiptsResult::fail();
};
// Fetch the block and the receipts within it
let Ok(block) = provider.block_by_hash(block_hash.into()) else {
return ReceiptsResult::fail();
};
let Ok(receipts) = provider.receipts_by_block(BlockHashOrNumber::Hash(block_hash.into()))
else {
return ReceiptsResult::fail();
};
if let (Some(block), Some(receipts)) = (block, receipts) {
let block_number = block.number;
let base_fee = block.base_fee_per_gas;
let block_hash = block.hash_slow();
let Some(receipts) = block
.body
.into_iter()
.zip(receipts.clone())
.enumerate()
.map(|(idx, (tx, receipt))| {
let meta = TransactionMeta {
tx_hash: tx.hash,
index: idx as u64,
block_hash,
block_number,
base_fee,
excess_blob_gas: None,
};
build_transaction_receipt_with_block_receipts(tx, meta, receipt, &receipts)
})
.collect::<Option<Vec<_>>>()
else {
return ReceiptsResult::fail();
};
// Convert the receipts to JSON for transport
let Ok(mut receipts_json) = serde_json::to_string(&receipts) else {
return ReceiptsResult::fail();
};
let res =
ReceiptsResult::success(receipts_json.as_mut_ptr() as *mut char, receipts_json.len());
// Forget the `receipts_json` string so that its memory isn't freed by the
// borrow checker at the end of this scope
std::mem::forget(receipts_json); // Prevent Rust from freeing the memory
res
} else {
ReceiptsResult::fail()
}
read_receipts_inner(block_hash, block_hash_len, db_path).unwrap_or(ReceiptsResult::fail())
}
/// Free a string that was allocated in Rust and passed to C.
......@@ -152,88 +31,3 @@ pub unsafe extern "C" fn free_string(string: *mut c_char) {
let _ = std::ffi::CString::from_raw(string);
}
}
/// Builds a hydrated [TransactionReceipt] from information in the passed transaction,
/// receipt, and block receipts.
///
/// Returns [None] if the transaction's sender could not be recovered from the signature.
#[inline(always)]
fn build_transaction_receipt_with_block_receipts(
tx: TransactionSigned,
meta: TransactionMeta,
receipt: Receipt,
all_receipts: &[Receipt],
) -> Option<TransactionReceipt> {
let transaction = tx.clone().into_ecrecovered()?;
// get the previous transaction cumulative gas used
let gas_used = if meta.index == 0 {
receipt.cumulative_gas_used
} else {
let prev_tx_idx = (meta.index - 1) as usize;
all_receipts
.get(prev_tx_idx)
.map(|prev_receipt| receipt.cumulative_gas_used - prev_receipt.cumulative_gas_used)
.unwrap_or_default()
};
let mut res_receipt = TransactionReceipt {
transaction_hash: Some(meta.tx_hash),
transaction_index: U64::from(meta.index),
block_hash: Some(meta.block_hash),
block_number: Some(U256::from(meta.block_number)),
from: transaction.signer(),
to: None,
cumulative_gas_used: U256::from(receipt.cumulative_gas_used),
gas_used: Some(U256::from(gas_used)),
contract_address: None,
logs: Vec::with_capacity(receipt.logs.len()),
effective_gas_price: U128::from(transaction.effective_gas_price(meta.base_fee)),
transaction_type: tx.transaction.tx_type().into(),
// TODO pre-byzantium receipts have a post-transaction state root
state_root: None,
logs_bloom: receipt.bloom_slow(),
status_code: if receipt.success {
Some(U64::from(1))
} else {
Some(U64::from(0))
},
// EIP-4844 fields
blob_gas_price: None,
blob_gas_used: None,
};
match tx.transaction.kind() {
TransactionKind::Create => {
res_receipt.contract_address =
Some(transaction.signer().create(tx.transaction.nonce()));
}
TransactionKind::Call(addr) => {
res_receipt.to = Some(*addr);
}
}
// get number of logs in the block
let mut num_logs = 0;
for prev_receipt in all_receipts.iter().take(meta.index as usize) {
num_logs += prev_receipt.logs.len();
}
for (tx_log_idx, log) in receipt.logs.into_iter().enumerate() {
let rpclog = Log {
address: log.address,
topics: log.topics,
data: log.data,
block_hash: Some(meta.block_hash),
block_number: Some(U256::from(meta.block_number)),
transaction_hash: Some(meta.tx_hash),
transaction_index: Some(U256::from(meta.index)),
log_index: Some(U256::from(num_logs + tx_log_idx)),
removed: false,
};
res_receipt.logs.push(rpclog);
}
Some(res_receipt)
}
//! This module contains the logic for reading a block's fully hydrated receipts directly from the
//! [reth] database.
use anyhow::{anyhow, Result};
use reth::{
blockchain_tree::noop::NoopBlockchainTree,
primitives::{
BlockHashOrNumber, Receipt, TransactionKind, TransactionMeta, TransactionSigned, MAINNET,
U128, U256, U64,
},
providers::{providers::BlockchainProvider, BlockReader, ProviderFactory, ReceiptProvider},
rpc::types::{Log, TransactionReceipt},
utils::db::open_db_read_only,
};
use std::{ffi::c_char, path::Path};
/// A [ReceiptsResult] is a wrapper around a JSON string containing serialized [TransactionReceipt]s
/// as well as an error status that is compatible with FFI.
///
/// # Safety
/// - When the `error` field is false, the `data` pointer is guaranteed to be valid.
/// - When the `error` field is true, the `data` pointer is guaranteed to be null.
#[repr(C)]
pub struct ReceiptsResult {
data: *mut char,
data_len: usize,
error: bool,
}
impl ReceiptsResult {
/// Constructs a successful [ReceiptsResult] from a JSON string.
pub fn success(data: *mut char, data_len: usize) -> Self {
Self {
data,
data_len,
error: false,
}
}
/// Constructs a failing [ReceiptsResult] with a null pointer to the data.
pub fn fail() -> Self {
Self {
data: std::ptr::null_mut(),
data_len: 0,
error: true,
}
}
}
/// Read the receipts for a blockhash from the RETH database directly.
///
/// # Safety
/// - All possible nil pointer dereferences are checked, and the function will return a
/// failing [ReceiptsResult] if any are found.
#[inline(always)]
pub(crate) unsafe fn read_receipts_inner(
block_hash: *const u8,
block_hash_len: usize,
db_path: *const c_char,
) -> Result<ReceiptsResult> {
// Convert the raw pointer and length back to a Rust slice
let block_hash: [u8; 32] = {
if block_hash.is_null() {
anyhow::bail!("block_hash pointer is null");
}
std::slice::from_raw_parts(block_hash, block_hash_len)
}
.try_into()?;
// Convert the *const c_char to a Rust &str
let db_path_str = {
if db_path.is_null() {
anyhow::bail!("db path pointer is null");
}
std::ffi::CStr::from_ptr(db_path)
}
.to_str()?;
let db = open_db_read_only(Path::new(db_path_str), None).map_err(|e| anyhow!(e))?;
let factory = ProviderFactory::new(db, MAINNET.clone());
// Create a read-only BlockChainProvider
let provider = BlockchainProvider::new(factory, NoopBlockchainTree::default())?;
// Fetch the block and the receipts within it
let block = provider
.block_by_hash(block_hash.into())?
.ok_or(anyhow!("Failed to fetch block"))?;
let receipts = provider
.receipts_by_block(BlockHashOrNumber::Hash(block_hash.into()))?
.ok_or(anyhow!("Failed to fetch block receipts"))?;
let block_number = block.number;
let base_fee = block.base_fee_per_gas;
let block_hash = block.hash_slow();
let receipts = block
.body
.into_iter()
.zip(receipts.clone())
.enumerate()
.map(|(idx, (tx, receipt))| {
let meta = TransactionMeta {
tx_hash: tx.hash,
index: idx as u64,
block_hash,
block_number,
base_fee,
excess_blob_gas: None,
};
build_transaction_receipt_with_block_receipts(tx, meta, receipt, &receipts)
})
.collect::<Option<Vec<_>>>()
.ok_or(anyhow!("Failed to build receipts"))?;
// Convert the receipts to JSON for transport
let mut receipts_json = serde_json::to_string(&receipts)?;
// Create a ReceiptsResult with a pointer to the json-ified receipts
let res = ReceiptsResult::success(receipts_json.as_mut_ptr() as *mut char, receipts_json.len());
// Forget the `receipts_json` string so that its memory isn't freed by the
// borrow checker at the end of this scope
std::mem::forget(receipts_json); // Prevent Rust from freeing the memory
Ok(res)
}
/// Builds a hydrated [TransactionReceipt] from information in the passed transaction,
/// receipt, and block receipts.
///
/// Returns [None] if the transaction's sender could not be recovered from the signature.
#[inline(always)]
fn build_transaction_receipt_with_block_receipts(
tx: TransactionSigned,
meta: TransactionMeta,
receipt: Receipt,
all_receipts: &[Receipt],
) -> Option<TransactionReceipt> {
let transaction = tx.clone().into_ecrecovered()?;
// get the previous transaction cumulative gas used
let gas_used = if meta.index == 0 {
receipt.cumulative_gas_used
} else {
let prev_tx_idx = (meta.index - 1) as usize;
all_receipts
.get(prev_tx_idx)
.map(|prev_receipt| receipt.cumulative_gas_used - prev_receipt.cumulative_gas_used)
.unwrap_or_default()
};
let mut res_receipt = TransactionReceipt {
transaction_hash: Some(meta.tx_hash),
transaction_index: U64::from(meta.index),
block_hash: Some(meta.block_hash),
block_number: Some(U256::from(meta.block_number)),
from: transaction.signer(),
to: None,
cumulative_gas_used: U256::from(receipt.cumulative_gas_used),
gas_used: Some(U256::from(gas_used)),
contract_address: None,
logs: Vec::with_capacity(receipt.logs.len()),
effective_gas_price: U128::from(transaction.effective_gas_price(meta.base_fee)),
transaction_type: tx.transaction.tx_type().into(),
// TODO pre-byzantium receipts have a post-transaction state root
state_root: None,
logs_bloom: receipt.bloom_slow(),
status_code: if receipt.success {
Some(U64::from(1))
} else {
Some(U64::from(0))
},
// EIP-4844 fields
blob_gas_price: None,
blob_gas_used: None,
};
match tx.transaction.kind() {
TransactionKind::Create => {
res_receipt.contract_address =
Some(transaction.signer().create(tx.transaction.nonce()));
}
TransactionKind::Call(addr) => {
res_receipt.to = Some(*addr);
}
}
// get number of logs in the block
let mut num_logs = 0;
for prev_receipt in all_receipts.iter().take(meta.index as usize) {
num_logs += prev_receipt.logs.len();
}
for (tx_log_idx, log) in receipt.logs.into_iter().enumerate() {
let rpclog = Log {
address: log.address,
topics: log.topics,
data: log.data,
block_hash: Some(meta.block_hash),
block_number: Some(U256::from(meta.block_number)),
transaction_hash: Some(meta.tx_hash),
transaction_index: Some(U256::from(meta.index)),
log_index: Some(U256::from(num_logs + tx_log_idx)),
removed: false,
};
res_receipt.logs.push(rpclog);
}
Some(res_receipt)
}
package sources
import (
"testing"
"github.com/ethereum/go-ethereum/common"
)
func TestRethDBRead(t *testing.T) {
t.Parallel()
_, err := FetchRethReceipts("/test", &common.Hash{})
if err != nil {
panic("test")
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment