diff --git a/crates/db-utils/src/leveldb/key.rs b/crates/db-utils/src/leveldb/key.rs index e54c07a..6d4392e 100644 --- a/crates/db-utils/src/leveldb/key.rs +++ b/crates/db-utils/src/leveldb/key.rs @@ -1,8 +1,57 @@ +//! Helpers for dealing with Geth's rawdb schema and constructing keys. +//! +//! See: https://github.com/ethereum/go-ethereum/blob/c8a22020287e0260e2310a1b91a1aa9b795ca445/core/rawdb/schema.go + use leveldb::database::key::Key; +pub const HEADER_PREFIX: u8 = b"h"[0]; +pub const HEADER_HASH_SUFFIX: u8 = b"n"[0]; +pub const BLOCK_BODY_PREFIX: u8 = b"b"[0]; + /// Wrapper around a [Vec] to implement the [Key] trait. pub struct DBKey(Vec); +impl DBKey { + /// Get a key for the `hash by number` table in the Geth leveldb. + /// + /// Format: `header_hash_prefix ++ number ++ header_hash_suffix -> header_hash` + pub fn hash_by_number(number: u64) -> Self { + const KEY_SIZE: usize = 1 + 8 + 1; + + let mut key = Vec::with_capacity(KEY_SIZE); + key.push(HEADER_PREFIX); + key.extend_from_slice(&number.to_be_bytes()); + key.push(HEADER_HASH_SUFFIX); + Self(key) + } + + /// Get a key for the `header by number + hash` table in the Geth leveldb. + /// + /// Format: `header_prefix ++ number ++ hash -> header_rlp` + pub fn header_lookup(hash: [u8; 32], number: u64) -> Self { + const KEY_SIZE: usize = 1 + 8 + 32; + + let mut key = Vec::with_capacity(KEY_SIZE); + key.push(HEADER_PREFIX); + key.extend_from_slice(&number.to_be_bytes()); + key.extend_from_slice(&hash); + Self(key) + } + + /// Get a key for the `block by number + hash` table in the Geth leveldb. + /// + /// Format: `block body prefix ++ number ++ hash -> block_body_rlp` + pub fn body_by_hash(hash: [u8; 32], number: u64) -> Self { + const KEY_SIZE: usize = 1 + 8 + 32; + + let mut key = Vec::with_capacity(KEY_SIZE); + key.push(BLOCK_BODY_PREFIX); + key.extend_from_slice(&number.to_be_bytes()); + key.extend_from_slice(&hash); + Self(key) + } +} + impl From> for DBKey { fn from(key: Vec) -> Self { Self(key) diff --git a/crates/db-utils/src/leveldb/mod.rs b/crates/db-utils/src/leveldb/mod.rs index e2bf3c5..08bcfe3 100644 --- a/crates/db-utils/src/leveldb/mod.rs +++ b/crates/db-utils/src/leveldb/mod.rs @@ -1,20 +1,99 @@ +use alloy_rlp::Decodable; +use anyhow::{anyhow, Result}; +use leveldb::{database::Database, kv::KV, options::ReadOptions}; +use reth_primitives::{Header, SealedBlock, SealedHeader, TransactionSigned}; + mod key; +pub use self::key::DBKey; + +/// A wrapper around a [leveldb] [Database] instance to read data from a Geth database into +/// [reth_primitives] types. +pub struct GethDBReader { + db: Database, +} + +impl GethDBReader { + /// Create a new [GethDBReader] instance. + /// + /// ### Takes + /// - `database`: An open handle to a Geth [leveldb] database. + pub fn new(database: Database) -> Self { + Self { db: database } + } + + /// Retrieve a [SealedHeader] by its block number from a Geth LevelDB + /// + /// ### Takes + /// - `db`: A reference to a [Database] instance + /// - `number`: The block number of the [SealedHeader] to retrieve + /// + /// ### Returns + /// - Success: A [SealedHeader] instance + /// - Failure: An [anyhow::Error] if the header could not be found + pub fn header_by_number(&self, number: u64) -> Result { + // Fetch the header hash + let header_hash_key = DBKey::hash_by_number(number); + let header_hash: [u8; 32] = self + .db + .get(ReadOptions::new(), header_hash_key)? + .ok_or(anyhow::anyhow!("Header hash not found"))? + .try_into() + .map_err(|_| anyhow!("Header hash received from DB is not 32 bytes in size"))?; + + // Fetch the header RLP + let header_key = DBKey::header_lookup(header_hash, number); + let header_rlp = self + .db + .get(ReadOptions::new(), header_key)? + .ok_or(anyhow::anyhow!("Header RLP not found"))?; + + // Decode the header + let unsealed_header = Header::decode(&mut header_rlp.as_slice()) + .map_err(|e| anyhow!("RLP decode error: {e}"))?; + + // Return the sealed header + Ok(unsealed_header.seal(header_hash.into())) + } + + /// Retrieve a [SealedBlock] by its block number from a Geth LevelDB. + /// + /// ### Takes + /// - `db`: A reference to a [Database] instance + /// - `number`: The block number of the [SealedBlock] to Retrieve + /// + /// ### Returns + /// - Success: A [SealedBlock] instance + /// - Failure: An [anyhow::Error] if the block could not be found + pub fn block_by_number(&self, number: u64) -> Result { + let header = self.header_by_number(number)?; + + let body_key = DBKey::body_by_hash(*header.hash, header.number); + let body_rlp = self + .db + .get(ReadOptions::new(), body_key)? + .ok_or(anyhow::anyhow!("Body RLP not found"))?; + + // Decode the transactions in the block body + // Why geth? ... Why? + let mut transactions = >>::decode(&mut body_rlp.as_slice())?; + + Ok(SealedBlock { + header, + body: transactions.remove(0), + ommers: Vec::new(), + withdrawals: None, + }) + } +} #[cfg(test)] mod db_test { use super::key::DBKey; - use alloy_primitives::hex; - use alloy_rlp::Decodable; - use leveldb::{ - database::Database, - kv::KV, - options::{Options, ReadOptions}, - }; - use reth_primitives::Header; + use crate::leveldb::GethDBReader; + use leveldb::{database::Database, options::Options}; use std::path::PathBuf; - const HEADER_PREFIX: u8 = b"h"[0]; - const NUM_SUFFIX: u8 = b"n"[0]; + const TEST_BLOCK_NO: u64 = 4_000_000; #[test] #[ignore] @@ -24,41 +103,21 @@ mod db_test { let options = Options::new(); let database: Database = Database::open(db_path.as_path(), options).unwrap(); + let reader = GethDBReader::new(database); + + dbg!(reader.header_by_number(TEST_BLOCK_NO).unwrap()); + } + + #[test] + #[ignore] + fn sanity_read_block() { + let mut db_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + db_path.push("testdata/geth/chaindata"); + + let options = Options::new(); + let database: Database = Database::open(db_path.as_path(), options).unwrap(); + let reader = GethDBReader::new(database); - let block_number = 4_000_000u64; - - // Formulate the block hash key - // Table 1: header prefix ++ number ++ num suffix - let mut block_hash_key = vec![HEADER_PREFIX]; - block_hash_key.extend_from_slice(&block_number.to_be_bytes()); - block_hash_key.push(NUM_SUFFIX); - - // Get blockhash first - let read_opts = ReadOptions::new(); - let block_hash = database - .get(read_opts, &block_hash_key.into()) - .unwrap() - .unwrap(); - - println!( - "Found block hash for block #{block_number}: 0x{}", - hex::encode(&block_hash) - ); - - // Formulate the header key - // Table 2: header prefix ++ number ++ hash - let mut header_key = vec![HEADER_PREFIX]; - header_key.extend_from_slice(&block_number.to_be_bytes()); - header_key.extend(block_hash); - - let read_opts = ReadOptions::new(); - let header = database - .get(read_opts, &header_key.into()) - .unwrap() - .unwrap(); - - // RLP Decode the header - let header = Header::decode(&mut header.as_slice()).unwrap(); - dbg!(header); + dbg!(reader.block_by_number(TEST_BLOCK_NO).unwrap()); } }