Commit 80d619ab authored by clabby's avatar clabby

PoC init

parent 48c735e9
This diff is collapsed.
[package]
name = "rethdb-reader"
description = "A simple library for reading data through Reth's DB abstractions."
version = "0.1.0"
edition = "2021"
[lib]
name = "rethdbreader"
crate-type = ["cdylib"]
[dependencies]
reth = { git = "https://github.com/paradigmxyz/reth.git" }
# `rethdb-reader`
Exported Rust code to be used via FFI in `op-service`'s `sources` package for reading information
directly from the `reth` database.
use reth::{
blockchain_tree::noop::NoopBlockchainTree,
primitives::{
alloy_primitives::private::alloy_rlp::Encodable, BlockHashOrNumber, ChainSpecBuilder,
},
providers::{providers::BlockchainProvider, ProviderFactory, ReceiptProvider},
utils::db::open_db_read_only,
};
use std::{os::raw::c_char, path::Path, sync::Arc};
#[repr(C)]
pub struct ByteArray {
data: *mut u8,
len: usize,
}
#[repr(C)]
pub struct ByteArrays {
data: *mut ByteArray,
len: usize,
}
/// Read the receipts for a blockhash from the RETH database directly.
///
/// WARNING: Will panic on error.
/// TODO: Gracefully return OK status.
#[no_mangle]
pub extern "C" fn read_receipts(
block_hash: *const u8,
block_hash_len: usize,
db_path: *const c_char,
) -> ByteArrays {
// Convert the raw pointer and length back to a Rust slice
let block_hash: [u8; 32] = unsafe { std::slice::from_raw_parts(block_hash, block_hash_len) }
.try_into()
.expect("Block hash must be 32 bytes long");
// Convert the *const c_char to a Rust &str
let db_path_str = unsafe {
assert!(!db_path.is_null(), "Null pointer for database path");
std::ffi::CStr::from_ptr(db_path)
.to_str()
.expect("Invalid UTF-8 for database path")
};
let db = open_db_read_only(&Path::new(db_path_str), None).expect("Could not open reth DB");
let spec = Arc::new(ChainSpecBuilder::mainnet().build());
let factory = ProviderFactory::new(db, spec.clone());
// Create a read-only BlockChainProvider
let provider = BlockchainProvider::new(factory, NoopBlockchainTree::default())
.expect("Failed to create blockchain provider.");
let receipts = provider
.receipts_by_block(BlockHashOrNumber::Hash(block_hash.into()))
.expect("Could not fetch receipts for block")
.expect("No receipts found for block");
// Serialize receipts to RLP for the FFI interface.
let receipts_rlp = receipts
.into_iter()
.map(|r| {
// todo - reduce alloc?
// RLP encode the receipt with a bloom filter.
let mut buf = Vec::default();
r.with_bloom().encode(&mut buf);
// Return a pointer to the `buf` and its length
let res = ByteArray {
data: buf.as_mut_ptr(),
len: buf.len(),
};
// Forget the `buf` so that its memory isn't freed by the
// borrow checker at the end of this scope
std::mem::forget(buf);
res
})
.collect::<Vec<_>>();
let result = ByteArrays {
data: receipts_rlp.as_ptr() as *mut ByteArray,
len: receipts_rlp.len(),
};
// Forget the `receipts_rlp` arr so that its memory isn't freed by the
// borrow checker at the end of this scope
std::mem::forget(receipts_rlp); // Prevent Rust from freeing the memory
result
}
/// Free the [ByteArrays] data structure and its sub-components when they are no longer needed.
#[no_mangle]
pub extern "C" fn free_byte_arrays(array: ByteArrays) {
unsafe {
let arrays = Vec::from_raw_parts(array.data, array.len, array.len);
for inner_array in arrays {
let _ = Vec::from_raw_parts(inner_array.data, inner_array.len, inner_array.len);
}
}
}
...@@ -124,6 +124,7 @@ const ( ...@@ -124,6 +124,7 @@ const (
RPCKindBasic RPCProviderKind = "basic" // try only the standard most basic receipt fetching RPCKindBasic RPCProviderKind = "basic" // try only the standard most basic receipt fetching
RPCKindAny RPCProviderKind = "any" // try any method available RPCKindAny RPCProviderKind = "any" // try any method available
RPCKindStandard RPCProviderKind = "standard" // try standard methods, including newer optimized standard RPC methods RPCKindStandard RPCProviderKind = "standard" // try standard methods, including newer optimized standard RPC methods
RPCKindRethDB RPCProviderKind = "reth_db" // read data directly from reth's MDBX database
) )
var RPCProviderKinds = []RPCProviderKind{ var RPCProviderKinds = []RPCProviderKind{
...@@ -137,6 +138,7 @@ var RPCProviderKinds = []RPCProviderKind{ ...@@ -137,6 +138,7 @@ var RPCProviderKinds = []RPCProviderKind{
RPCKindBasic, RPCKindBasic,
RPCKindAny, RPCKindAny,
RPCKindStandard, RPCKindStandard,
RPCKindRethDB,
} }
func (kind RPCProviderKind) String() string { func (kind RPCProviderKind) String() string {
...@@ -268,6 +270,18 @@ const ( ...@@ -268,6 +270,18 @@ const (
// See: // See:
// https://github.com/ledgerwatch/erigon/blob/287a3d1d6c90fc6a7a088b5ae320f93600d5a167/cmd/rpcdaemon/commands/erigon_receipts.go#LL391C24-L391C51 // https://github.com/ledgerwatch/erigon/blob/287a3d1d6c90fc6a7a088b5ae320f93600d5a167/cmd/rpcdaemon/commands/erigon_receipts.go#LL391C24-L391C51
ErigonGetBlockReceiptsByBlockHash ErigonGetBlockReceiptsByBlockHash
// RethGetBlockReceiptsMDBX is a Reth-specific receipt fetching method. It reads the data directly from reth's database, using their
// generic DB abstractions, rather than requesting it from the RPC provider.
// Available in:
// - Reth
// Method: n/a - does not use RPC.
// Params:
// - Reth: string, hex-encoded block hash
// Returns:
// - Reth: array of RLP-encoded receipts
// See:
// - reth's DB crate documentation: https://github.com/paradigmxyz/reth/blob/main/docs/crates/db.md
RethGetBlockReceiptsMDBX
// Other: // Other:
// - 250 credits, not supported, strictly worse than other options. In quicknode price-table. // - 250 credits, not supported, strictly worse than other options. In quicknode price-table.
...@@ -297,12 +311,14 @@ func AvailableReceiptsFetchingMethods(kind RPCProviderKind) ReceiptsFetchingMeth ...@@ -297,12 +311,14 @@ func AvailableReceiptsFetchingMethods(kind RPCProviderKind) ReceiptsFetchingMeth
case RPCKindBasic: case RPCKindBasic:
return EthGetTransactionReceiptBatch return EthGetTransactionReceiptBatch
case RPCKindAny: case RPCKindAny:
// if it's any kind of RPC provider, then try all methods // if it's any kind of RPC provider, then try all methods (except for RethGetBlockReceiptsMDBX)
return AlchemyGetTransactionReceipts | EthGetBlockReceipts | return AlchemyGetTransactionReceipts | EthGetBlockReceipts |
DebugGetRawReceipts | ErigonGetBlockReceiptsByBlockHash | DebugGetRawReceipts | ErigonGetBlockReceiptsByBlockHash |
ParityGetBlockReceipts | EthGetTransactionReceiptBatch ParityGetBlockReceipts | EthGetTransactionReceiptBatch
case RPCKindStandard: case RPCKindStandard:
return EthGetBlockReceipts | EthGetTransactionReceiptBatch return EthGetBlockReceipts | EthGetTransactionReceiptBatch
case RPCKindRethDB:
return RethGetBlockReceiptsMDBX
default: default:
return EthGetTransactionReceiptBatch return EthGetTransactionReceiptBatch
} }
...@@ -313,7 +329,9 @@ func AvailableReceiptsFetchingMethods(kind RPCProviderKind) ReceiptsFetchingMeth ...@@ -313,7 +329,9 @@ func AvailableReceiptsFetchingMethods(kind RPCProviderKind) ReceiptsFetchingMeth
func PickBestReceiptsFetchingMethod(kind RPCProviderKind, available ReceiptsFetchingMethod, txCount uint64) ReceiptsFetchingMethod { func PickBestReceiptsFetchingMethod(kind RPCProviderKind, available ReceiptsFetchingMethod, txCount uint64) ReceiptsFetchingMethod {
// If we have optimized methods available, it makes sense to use them, but only if the cost is // If we have optimized methods available, it makes sense to use them, but only if the cost is
// lower than fetching transactions one by one with the standard receipts RPC method. // lower than fetching transactions one by one with the standard receipts RPC method.
if kind == RPCKindAlchemy { if kind == RPCKindRethDB {
return RethGetBlockReceiptsMDBX
} else if kind == RPCKindAlchemy {
if available&AlchemyGetTransactionReceipts != 0 && txCount > 250/15 { if available&AlchemyGetTransactionReceipts != 0 && txCount > 250/15 {
return AlchemyGetTransactionReceipts return AlchemyGetTransactionReceipts
} }
...@@ -460,6 +478,12 @@ func (job *receiptsFetchingJob) runAltMethod(ctx context.Context, m ReceiptsFetc ...@@ -460,6 +478,12 @@ func (job *receiptsFetchingJob) runAltMethod(ctx context.Context, m ReceiptsFetc
err = job.client.CallContext(ctx, &result, "eth_getBlockReceipts", job.block.Hash) err = job.client.CallContext(ctx, &result, "eth_getBlockReceipts", job.block.Hash)
case ErigonGetBlockReceiptsByBlockHash: case ErigonGetBlockReceiptsByBlockHash:
err = job.client.CallContext(ctx, &result, "erigon_getBlockReceiptsByBlockHash", job.block.Hash) err = job.client.CallContext(ctx, &result, "erigon_getBlockReceiptsByBlockHash", job.block.Hash)
case RethGetBlockReceiptsMDBX:
res, err := FetchRethReceipts("placeholder", &job.block.Hash)
if err != nil {
return err
}
result = res
default: default:
err = fmt.Errorf("unknown receipt fetching method: %d", uint64(m)) err = fmt.Errorf("unknown receipt fetching method: %d", uint64(m))
} }
......
package sources
import (
"fmt"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
)
/*
#cgo LDFLAGS: -L../rethdb-reader/target/release -lrethdbreader
#include <stdlib.h>
#include <stdint.h>
typedef struct {
uint8_t* data;
size_t len;
} ByteArray;
typedef struct {
ByteArray* data;
size_t len;
} ByteArrays;
extern ByteArrays read_receipts(const uint8_t* block_hash, size_t block_hash_len, const char* db_path);
extern void free_byte_arrays(ByteArrays arrays);
*/
import "C"
import "unsafe"
// FetchRethReceipts fetches the receipts for the given block hash directly from the Reth Database
// and populates the given results slice pointer with the receipts that were found.
func FetchRethReceipts(dbPath string, blockHash *common.Hash) (types.Receipts, error) {
if blockHash == nil {
return nil, fmt.Errorf("Must provide a block hash to fetch receipts for.")
}
// Convert the block hash to a C byte array and defer its deallocation
cBlockHash := C.CBytes(blockHash[:])
defer C.free(cBlockHash)
// Convert the db path to a C string and defer its deallocation
cDbPath := C.CString(dbPath)
defer C.free(unsafe.Pointer(cDbPath))
// Call the C function to fetch the receipts from the Reth Database
byteArrayStruct := C.read_receipts((*C.uint8_t)(cBlockHash), C.size_t(len(blockHash)), cDbPath)
// Convert the returned receipt RLP byte arrays to decoded Receipts.
data := make(types.Receipts, byteArrayStruct.len)
byteArraySlice := (*[1 << 30]C.ByteArray)(unsafe.Pointer(byteArrayStruct.data))[:byteArrayStruct.len:byteArrayStruct.len]
for i, byteArray := range byteArraySlice {
receipt := types.Receipt{}
receipt.UnmarshalBinary(C.GoBytes(unsafe.Pointer(byteArray.data), C.int(byteArray.len)))
data[i] = &receipt
}
// Free the memory allocated by the C code
C.free_byte_arrays(byteArrayStruct)
return data, nil
}
package sources
import (
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
)
func TestRethReceiptsLoad(t *testing.T) {
t.Skip("Skipping test that requires a local L1 Goerli Reth DB")
t.Parallel()
// block = https://goerli.etherscan.io/block/994113
blockHash := common.HexToHash("0x6f6f00553e4f74262a9812927afd11c341730c5c9210824fe172367457adb5f6")
res, err := FetchRethReceipts("/path/to/goerli-db", &blockHash)
require.NoError(t, err, "Failed to fetch receipts from Reth DB")
require.Len(t, res, 2, "Expected 2 receipts to be returned")
require.Equal(t, res[0].Type, 0)
require.Equal(t, res[0].CumulativeGasUsed, uint64(93_787))
require.Equal(t, res[0].Status, uint64(1))
}
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