Breaking changes (#5191)

Co-authored-by: Bjerg <onbjerg@users.noreply.github.com>
Co-authored-by: Roman Krasiuk <rokrassyuk@gmail.com>
Co-authored-by: joshieDo <ranriver@protonmail.com>
Co-authored-by: joshieDo <93316087+joshieDo@users.noreply.github.com>
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
Co-authored-by: Oliver Nordbjerg <hi@notbjerg.me>
Co-authored-by: Thomas Coratger <thomas.coratger@gmail.com>
This commit is contained in:
Alexey Shekhirin
2024-02-29 12:37:28 +00:00
committed by GitHub
parent 025fa5f038
commit 6b5b6f7a40
252 changed files with 10154 additions and 6327 deletions

View File

@ -52,7 +52,7 @@ pub fn generate_from_to(ident: &Ident, fields: &FieldList, is_zstd: bool) -> Tok
/// Generates code to implement the `Compact` trait method `to_compact`.
fn generate_from_compact(fields: &FieldList, ident: &Ident, is_zstd: bool) -> TokenStream2 {
let mut lines = vec![];
let mut known_types = vec!["B256", "Address", "Bloom", "Vec", "TxHash"];
let mut known_types = vec!["B256", "Address", "Bloom", "Vec", "TxHash", "BlockHash"];
// Only types without `Bytes` should be added here. It's currently manually added, since
// it's hard to figure out with derive_macro which types have Bytes fields.

View File

@ -143,7 +143,7 @@ fn should_use_alt_impl(ftype: &String, segment: &syn::PathSegment) -> bool {
if let (Some(path), 1) =
(arg_path.path.segments.first(), arg_path.path.segments.len())
{
if ["B256", "Address", "Address", "Bloom", "TxHash"]
if ["B256", "Address", "Address", "Bloom", "TxHash", "BlockHash"]
.contains(&path.ident.to_string().as_str())
{
return true
@ -164,11 +164,6 @@ pub fn get_bit_size(ftype: &str) -> u8 {
"u64" | "BlockNumber" | "TxNumber" | "ChainId" | "NumTransactions" => 4,
"u128" => 5,
"U256" => 6,
#[cfg(not(feature = "optimism"))]
"TxValue" => 5, // u128 for ethereum chains assuming high order bits are not used
#[cfg(feature = "optimism")]
// for fuzz/prop testing and chains that may require full 256 bits
"TxValue" => 6,
_ => 0,
}
}

View File

@ -23,12 +23,12 @@ pub fn db(c: &mut Criterion) {
group.warm_up_time(std::time::Duration::from_millis(200));
measure_table_db::<CanonicalHeaders>(&mut group);
measure_table_db::<HeaderTD>(&mut group);
measure_table_db::<HeaderTerminalDifficulties>(&mut group);
measure_table_db::<HeaderNumbers>(&mut group);
measure_table_db::<Headers>(&mut group);
measure_table_db::<BlockBodyIndices>(&mut group);
measure_table_db::<BlockOmmers>(&mut group);
measure_table_db::<TxHashNumber>(&mut group);
measure_table_db::<TransactionHashNumbers>(&mut group);
measure_table_db::<Transactions>(&mut group);
measure_dupsort_db::<PlainStorageState>(&mut group);
measure_table_db::<PlainAccountState>(&mut group);
@ -40,12 +40,12 @@ pub fn serialization(c: &mut Criterion) {
group.warm_up_time(std::time::Duration::from_millis(200));
measure_table_serialization::<CanonicalHeaders>(&mut group);
measure_table_serialization::<HeaderTD>(&mut group);
measure_table_serialization::<HeaderTerminalDifficulties>(&mut group);
measure_table_serialization::<HeaderNumbers>(&mut group);
measure_table_serialization::<Headers>(&mut group);
measure_table_serialization::<BlockBodyIndices>(&mut group);
measure_table_serialization::<BlockOmmers>(&mut group);
measure_table_serialization::<TxHashNumber>(&mut group);
measure_table_serialization::<TransactionHashNumbers>(&mut group);
measure_table_serialization::<Transactions>(&mut group);
measure_table_serialization::<PlainStorageState>(&mut group);
measure_table_serialization::<PlainAccountState>(&mut group);

View File

@ -9,7 +9,7 @@ use proptest::{
strategy::{Strategy, ValueTree},
test_runner::TestRunner,
};
use reth_db::{cursor::DbCursorRW, TxHashNumber};
use reth_db::{cursor::DbCursorRW, TransactionHashNumbers};
use std::collections::HashSet;
criterion_group! {
@ -34,7 +34,7 @@ pub fn hash_keys(c: &mut Criterion) {
group.sample_size(10);
for size in [10_000, 100_000, 1_000_000] {
measure_table_insertion::<TxHashNumber>(&mut group, size);
measure_table_insertion::<TransactionHashNumbers>(&mut group, size);
}
}

View File

@ -79,12 +79,12 @@ macro_rules! impl_iai {
impl_iai!(
CanonicalHeaders,
HeaderTD,
HeaderTerminalDifficulties,
HeaderNumbers,
Headers,
BlockBodyIndices,
BlockOmmers,
TxHashNumber,
TransactionHashNumbers,
Transactions,
PlainStorageState,
PlainAccountState

View File

@ -232,7 +232,10 @@ impl DatabaseEnv {
}
};
inner_env.set_max_dbs(Tables::ALL.len());
// Note: We set max dbs to 256 here to allow for custom tables. This needs to be set on
// environment creation.
debug_assert!(Tables::ALL.len() <= 256, "number of tables exceed max dbs");
inner_env.set_max_dbs(256);
inner_env.set_geometry(Geometry {
// Maximum database size of 4 terabytes
size: Some(0..(4 * TERABYTE)),
@ -385,10 +388,12 @@ mod tests {
abstraction::table::{Encode, Table},
cursor::{DbCursorRO, DbCursorRW, DbDupCursorRO, DbDupCursorRW, ReverseWalker, Walker},
models::{AccountBeforeTx, ShardedKey},
tables::{AccountHistory, CanonicalHeaders, Headers, PlainAccountState, PlainStorageState},
tables::{
AccountsHistory, CanonicalHeaders, Headers, PlainAccountState, PlainStorageState,
},
test_utils::*,
transaction::{DbTx, DbTxMut},
AccountChangeSet,
AccountChangeSets,
};
use reth_interfaces::db::{DatabaseWriteError, DatabaseWriteOperation};
use reth_libmdbx::Error;
@ -544,24 +549,24 @@ mod tests {
let address2 = Address::with_last_byte(2);
let tx = db.tx_mut().expect(ERROR_INIT_TX);
tx.put::<AccountChangeSet>(0, AccountBeforeTx { address: address0, info: None })
tx.put::<AccountChangeSets>(0, AccountBeforeTx { address: address0, info: None })
.expect(ERROR_PUT);
tx.put::<AccountChangeSet>(0, AccountBeforeTx { address: address1, info: None })
tx.put::<AccountChangeSets>(0, AccountBeforeTx { address: address1, info: None })
.expect(ERROR_PUT);
tx.put::<AccountChangeSet>(0, AccountBeforeTx { address: address2, info: None })
tx.put::<AccountChangeSets>(0, AccountBeforeTx { address: address2, info: None })
.expect(ERROR_PUT);
tx.put::<AccountChangeSet>(1, AccountBeforeTx { address: address0, info: None })
tx.put::<AccountChangeSets>(1, AccountBeforeTx { address: address0, info: None })
.expect(ERROR_PUT);
tx.put::<AccountChangeSet>(1, AccountBeforeTx { address: address1, info: None })
tx.put::<AccountChangeSets>(1, AccountBeforeTx { address: address1, info: None })
.expect(ERROR_PUT);
tx.put::<AccountChangeSet>(1, AccountBeforeTx { address: address2, info: None })
tx.put::<AccountChangeSets>(1, AccountBeforeTx { address: address2, info: None })
.expect(ERROR_PUT);
tx.put::<AccountChangeSet>(2, AccountBeforeTx { address: address0, info: None }) // <- should not be returned by the walker
tx.put::<AccountChangeSets>(2, AccountBeforeTx { address: address0, info: None }) // <- should not be returned by the walker
.expect(ERROR_PUT);
tx.commit().expect(ERROR_COMMIT);
let tx = db.tx().expect(ERROR_INIT_TX);
let mut cursor = tx.cursor_read::<AccountChangeSet>().unwrap();
let mut cursor = tx.cursor_read::<AccountChangeSets>().unwrap();
let entries = cursor.walk_range(..).unwrap().collect::<Result<Vec<_>, _>>().unwrap();
assert_eq!(entries.len(), 7);
@ -958,7 +963,7 @@ mod tests {
let transition_id = 2;
let tx = db.tx_mut().expect(ERROR_INIT_TX);
let mut cursor = tx.cursor_write::<AccountChangeSet>().unwrap();
let mut cursor = tx.cursor_write::<AccountChangeSets>().unwrap();
vec![0, 1, 3, 4, 5]
.into_iter()
.try_for_each(|val| {
@ -973,7 +978,7 @@ mod tests {
// APPEND DUP & APPEND
let subkey_to_append = 2;
let tx = db.tx_mut().expect(ERROR_INIT_TX);
let mut cursor = tx.cursor_write::<AccountChangeSet>().unwrap();
let mut cursor = tx.cursor_write::<AccountChangeSets>().unwrap();
assert_eq!(
cursor.append_dup(
transition_id,
@ -982,7 +987,7 @@ mod tests {
Err(DatabaseWriteError {
info: Error::KeyMismatch.into(),
operation: DatabaseWriteOperation::CursorAppendDup,
table_name: AccountChangeSet::NAME,
table_name: AccountChangeSets::NAME,
key: transition_id.encode().into(),
}
.into())
@ -995,7 +1000,7 @@ mod tests {
Err(DatabaseWriteError {
info: Error::KeyMismatch.into(),
operation: DatabaseWriteOperation::CursorAppend,
table_name: AccountChangeSet::NAME,
table_name: AccountChangeSets::NAME,
key: (transition_id - 1).encode().into(),
}
.into())
@ -1184,13 +1189,14 @@ mod tests {
let key = ShardedKey::new(real_key, i * 100);
let list: IntegerList = vec![i * 100u64].into();
db.update(|tx| tx.put::<AccountHistory>(key.clone(), list.clone()).expect("")).unwrap();
db.update(|tx| tx.put::<AccountsHistory>(key.clone(), list.clone()).expect(""))
.unwrap();
}
// Seek value with non existing key.
{
let tx = db.tx().expect(ERROR_INIT_TX);
let mut cursor = tx.cursor_read::<AccountHistory>().unwrap();
let mut cursor = tx.cursor_read::<AccountsHistory>().unwrap();
// It will seek the one greater or equal to the query. Since we have `Address | 100`,
// `Address | 200` in the database and we're querying `Address | 150` it will return us
@ -1208,7 +1214,7 @@ mod tests {
// Seek greatest index
{
let tx = db.tx().expect(ERROR_INIT_TX);
let mut cursor = tx.cursor_read::<AccountHistory>().unwrap();
let mut cursor = tx.cursor_read::<AccountsHistory>().unwrap();
// It will seek the MAX value of transition index and try to use prev to get first
// biggers.

View File

@ -67,7 +67,7 @@ pub mod abstraction;
mod implementation;
mod metrics;
pub mod snapshot;
pub mod static_file;
pub mod tables;
mod utils;
pub mod version;
@ -98,7 +98,7 @@ pub fn init_db<P: AsRef<Path>>(path: P, args: DatabaseArguments) -> eyre::Result
let rpath = path.as_ref();
if is_database_empty(rpath) {
std::fs::create_dir_all(rpath)
reth_primitives::fs::create_dir_all(rpath)
.wrap_err_with(|| format!("Could not create database directory {}", rpath.display()))?;
create_db_version_file(rpath)?;
} else {
@ -163,6 +163,8 @@ pub mod test_utils {
pub const ERROR_DB_OPEN: &str = "Not able to open the database file.";
/// Error during database creation
pub const ERROR_DB_CREATION: &str = "Not able to create the database file.";
/// Error during database creation
pub const ERROR_STATIC_FILES_CREATION: &str = "Not able to create the static file path.";
/// Error during table creation
pub const ERROR_TABLE_CREATION: &str = "Not able to create tables in the database.";
/// Error during tempdir creation
@ -225,6 +227,15 @@ pub mod test_utils {
}
}
/// Create static_files path for testing
pub fn create_test_static_files_dir() -> PathBuf {
let path = tempdir_path();
let emsg = format!("{}: {:?}", ERROR_STATIC_FILES_CREATION, path);
reth_primitives::fs::create_dir_all(path.clone()).expect(&emsg);
path
}
/// Get a temporary directory path to use for the database
pub fn tempdir_path() -> PathBuf {
let builder = tempfile::Builder::new().prefix("reth-test-").rand_bytes(8).tempdir();

View File

@ -1,28 +0,0 @@
use super::{ReceiptMask, TransactionMask};
use crate::{
add_snapshot_mask,
snapshot::mask::{ColumnSelectorOne, ColumnSelectorTwo, HeaderMask},
table::Table,
CanonicalHeaders, HeaderTD, Receipts, Transactions,
};
use reth_primitives::{BlockHash, Header};
// HEADER MASKS
add_snapshot_mask!(HeaderMask, Header, 0b001);
add_snapshot_mask!(HeaderMask, <HeaderTD as Table>::Value, 0b010);
add_snapshot_mask!(HeaderMask, BlockHash, 0b100);
add_snapshot_mask!(HeaderMask, Header, BlockHash, 0b101);
add_snapshot_mask!(
HeaderMask,
<HeaderTD as Table>::Value,
<CanonicalHeaders as Table>::Value,
0b110
);
// RECEIPT MASKS
add_snapshot_mask!(ReceiptMask, <Receipts as Table>::Value, 0b1);
// TRANSACTION MASKS
add_snapshot_mask!(TransactionMask, <Transactions as Table>::Value, 0b1);

View File

@ -1,76 +0,0 @@
//! reth's snapshot database table import and access
mod generation;
use std::{
collections::{hash_map::Entry, HashMap},
ops::RangeInclusive,
path::Path,
};
pub use generation::*;
mod cursor;
pub use cursor::SnapshotCursor;
mod mask;
pub use mask::*;
use reth_nippy_jar::{NippyJar, NippyJarError};
use reth_primitives::{snapshot::SegmentHeader, BlockNumber, SnapshotSegment, TxNumber};
mod masks;
/// Alias type for a map of [`SnapshotSegment`] and sorted lists of existing snapshot ranges.
type SortedSnapshots =
HashMap<SnapshotSegment, Vec<(RangeInclusive<BlockNumber>, RangeInclusive<TxNumber>)>>;
/// Given the snapshots directory path, it returns a list over the existing snapshots organized by
/// [`SnapshotSegment`]. Each segment has a sorted list of block ranges and transaction ranges.
pub fn iter_snapshots(path: impl AsRef<Path>) -> Result<SortedSnapshots, NippyJarError> {
let mut static_files = SortedSnapshots::default();
let entries = reth_primitives::fs::read_dir(path.as_ref())
.map_err(|err| NippyJarError::Custom(err.to_string()))?
.filter_map(Result::ok)
.collect::<Vec<_>>();
for entry in entries {
if entry.metadata().map_or(false, |metadata| metadata.is_file()) {
if let Some((segment, block_range, tx_range)) =
SnapshotSegment::parse_filename(&entry.file_name())
{
let ranges = (block_range, tx_range);
match static_files.entry(segment) {
Entry::Occupied(mut entry) => {
entry.get_mut().push(ranges);
}
Entry::Vacant(entry) => {
entry.insert(vec![ranges]);
}
}
}
}
}
for (segment, range_list) in static_files.iter_mut() {
// Sort by block end range.
range_list.sort_by(|a, b| a.0.end().cmp(b.0.end()));
if let Some((block_range, tx_range)) = range_list.pop() {
// The highest height static file filename might not be indicative of its actual
// block_range, so we need to read its actual configuration.
let jar = NippyJar::<SegmentHeader>::load(
&path.as_ref().join(segment.filename(&block_range, &tx_range)),
)?;
if &tx_range != jar.user_header().tx_range() {
// TODO(joshie): rename
}
range_list.push((
jar.user_header().block_range().clone(),
jar.user_header().tx_range().clone(),
));
}
}
Ok(static_files)
}

View File

@ -3,23 +3,23 @@ use crate::table::Decompress;
use derive_more::{Deref, DerefMut};
use reth_interfaces::provider::ProviderResult;
use reth_nippy_jar::{DataReader, NippyJar, NippyJarCursor};
use reth_primitives::{snapshot::SegmentHeader, B256};
use reth_primitives::{static_file::SegmentHeader, B256};
use std::sync::Arc;
/// Cursor of a snapshot segment.
/// Cursor of a static file segment.
#[derive(Debug, Deref, DerefMut)]
pub struct SnapshotCursor<'a>(NippyJarCursor<'a, SegmentHeader>);
pub struct StaticFileCursor<'a>(NippyJarCursor<'a, SegmentHeader>);
impl<'a> SnapshotCursor<'a> {
/// Returns a new [`SnapshotCursor`].
impl<'a> StaticFileCursor<'a> {
/// Returns a new [`StaticFileCursor`].
pub fn new(jar: &'a NippyJar<SegmentHeader>, reader: Arc<DataReader>) -> ProviderResult<Self> {
Ok(Self(NippyJarCursor::with_reader(jar, reader)?))
}
/// Returns the current `BlockNumber` or `TxNumber` of the cursor depending on the kind of
/// snapshot segment.
pub fn number(&self) -> u64 {
self.row_index() + self.jar().user_header().start()
/// static file segment.
pub fn number(&self) -> Option<u64> {
self.jar().user_header().start().map(|start| self.row_index() + start)
}
/// Gets a row of values.
@ -28,15 +28,21 @@ impl<'a> SnapshotCursor<'a> {
key_or_num: KeyOrNumber<'_>,
mask: usize,
) -> ProviderResult<Option<Vec<&'_ [u8]>>> {
if self.jar().rows() == 0 {
return Ok(None)
}
let row = match key_or_num {
KeyOrNumber::Key(k) => self.row_by_key_with_cols(k, mask),
KeyOrNumber::Number(n) => {
let offset = self.jar().user_header().start();
if offset > n {
return Ok(None)
KeyOrNumber::Number(n) => match self.jar().user_header().start() {
Some(offset) => {
if offset > n {
return Ok(None)
}
self.row_by_number_with_cols((n - offset) as usize, mask)
}
self.row_by_number_with_cols((n - offset) as usize, mask)
}
None => Ok(None),
},
}?;
Ok(row)

View File

@ -10,16 +10,16 @@ use reth_nippy_jar::{ColumnResult, NippyJar, NippyJarHeader, PHFKey};
use reth_tracing::tracing::*;
use std::{error::Error as StdError, ops::RangeInclusive};
/// Macro that generates snapshot creation functions that take an arbitratry number of [`Table`] and
/// creates a [`NippyJar`] file out of their [`Table::Value`]. Each list of [`Table::Value`] from a
/// table is a column of values.
/// Macro that generates static file creation functions that take an arbitratry number of [`Table`]
/// and creates a [`NippyJar`] file out of their [`Table::Value`]. Each list of [`Table::Value`]
/// from a table is a column of values.
///
/// Has membership filter set and compression dictionary support.
macro_rules! generate_snapshot_func {
macro_rules! generate_static_file_func {
($(($($tbl:ident),+)),+ $(,)? ) => {
$(
paste::item! {
/// Creates a snapshot from specified tables. Each table's `Value` iterator represents a column.
/// Creates a static file from specified tables. Each table's `Value` iterator represents a column.
///
/// **Ensure the range contains the same number of rows.**
///
@ -29,9 +29,9 @@ macro_rules! generate_snapshot_func {
/// * `keys`: Iterator of keys (eg. `TxHash` or `BlockHash`) with length equal to `row_count` and ordered by future column insertion from `range`.
/// * `dict_compression_set`: Sets of column data for compression dictionaries. Max size is 2GB. Row count is independent.
/// * `row_count`: Total rows to add to `NippyJar`. Must match row count in `range`.
/// * `nippy_jar`: Snapshot object responsible for file generation.
/// * `nippy_jar`: Static File object responsible for file generation.
#[allow(non_snake_case)]
pub fn [<create_snapshot$(_ $tbl)+>]<
pub fn [<create_static_file$(_ $tbl)+>]<
$($tbl: Table<Key=K>,)+
K,
H: NippyJarHeader
@ -43,27 +43,27 @@ macro_rules! generate_snapshot_func {
dict_compression_set: Option<Vec<impl Iterator<Item = Vec<u8>>>>,
keys: Option<impl Iterator<Item = ColumnResult<impl PHFKey>>>,
row_count: usize,
nippy_jar: &mut NippyJar<H>
mut nippy_jar: NippyJar<H>
) -> ProviderResult<()>
where K: Key + Copy
{
let additional = additional.unwrap_or_default();
debug!(target: "reth::snapshot", ?range, "Creating snapshot {:?} and {} more columns.", vec![$($tbl::NAME,)+], additional.len());
debug!(target: "reth::static_file", ?range, "Creating static file {:?} and {} more columns.", vec![$($tbl::NAME,)+], additional.len());
let range: RangeInclusive<RawKey<K>> = RawKey::new(*range.start())..=RawKey::new(*range.end());
// Create PHF and Filter if required
if let Some(keys) = keys {
debug!(target: "reth::snapshot", "Calculating Filter, PHF and offset index list");
debug!(target: "reth::static_file", "Calculating Filter, PHF and offset index list");
nippy_jar.prepare_index(keys, row_count)?;
debug!(target: "reth::snapshot", "Filter, PHF and offset index list calculated.");
debug!(target: "reth::static_file", "Filter, PHF and offset index list calculated.");
}
// Create compression dictionaries if required
if let Some(data_sets) = dict_compression_set {
debug!(target: "reth::snapshot", "Creating compression dictionaries.");
debug!(target: "reth::static_file", "Creating compression dictionaries.");
nippy_jar.prepare_compression(data_sets)?;
debug!(target: "reth::snapshot", "Compression dictionaries created.");
debug!(target: "reth::static_file", "Compression dictionaries created.");
}
// Creates the cursors for the columns
@ -80,17 +80,17 @@ macro_rules! generate_snapshot_func {
)+
// Create the snapshot from the data
// Create the static file from the data
let col_iterators: Vec<Box<dyn Iterator<Item = Result<Vec<u8>,_>>>> = vec![
$(Box::new([< $tbl _iter>]),)+
];
debug!(target: "reth::snapshot", jar=?nippy_jar, "Generating snapshot file.");
debug!(target: "reth::static_file", jar=?nippy_jar, "Generating static file.");
nippy_jar.freeze(col_iterators.into_iter().chain(additional).collect(), row_count as u64)?;
let nippy_jar = nippy_jar.freeze(col_iterators.into_iter().chain(additional).collect(), row_count as u64)?;
debug!(target: "reth::snapshot", jar=?nippy_jar, "Snapshot file generated.");
debug!(target: "reth::static_file", jar=?nippy_jar, "Static file generated.");
Ok(())
}
@ -99,4 +99,4 @@ macro_rules! generate_snapshot_func {
};
}
generate_snapshot_func!((T1), (T1, T2), (T1, T2, T3), (T1, T2, T3, T4), (T1, T2, T3, T4, T5),);
generate_static_file_func!((T1), (T1, T2), (T1, T2, T3), (T1, T2, T3, T4), (T1, T2, T3, T4, T5),);

View File

@ -4,14 +4,14 @@ use crate::table::Decompress;
///
/// #### Explanation:
///
/// A `NippyJar` snapshot row can contain multiple column values. To specify the column values
/// A `NippyJar` static file row can contain multiple column values. To specify the column values
/// to be read, a mask is utilized.
///
/// For example, a snapshot with three columns, if the first and last columns are queried, the mask
/// `0b101` would be passed. To select only the second column, the mask `0b010` would be used.
/// For example, a static file with three columns, if the first and last columns are queried, the
/// mask `0b101` would be passed. To select only the second column, the mask `0b010` would be used.
///
/// Since each snapshot has its own column distribution, different wrapper types are necessary. For
/// instance, `B256` might be the third column in the `Header` segment, while being the second
/// Since each static file has its own column distribution, different wrapper types are necessary.
/// For instance, `B256` might be the third column in the `Header` segment, while being the second
/// column in another segment. Hence, `Mask<B256>` would only be applicable to one of these
/// scenarios.
///
@ -24,7 +24,7 @@ macro_rules! add_segments {
($($segment:tt),+) => {
paste::paste! {
$(
#[doc = concat!("Mask for ", stringify!($segment), " snapshot segment. See [`Mask`] for more.")]
#[doc = concat!("Mask for ", stringify!($segment), " static file segment. See [`Mask`] for more.")]
#[derive(Debug)]
pub struct [<$segment Mask>]<FIRST, SECOND = (), THIRD = ()>(Mask<FIRST, SECOND, THIRD>);
)+
@ -37,7 +37,7 @@ add_segments!(Header, Receipt, Transaction);
pub trait ColumnSelectorOne {
/// First desired column value
type FIRST: Decompress;
/// Mask to obtain desired values, should correspond to the order of columns in a snapshot.
/// Mask to obtain desired values, should correspond to the order of columns in a static_file.
const MASK: usize;
}
@ -47,7 +47,7 @@ pub trait ColumnSelectorTwo {
type FIRST: Decompress;
/// Second desired column value
type SECOND: Decompress;
/// Mask to obtain desired values, should correspond to the order of columns in a snapshot.
/// Mask to obtain desired values, should correspond to the order of columns in a static_file.
const MASK: usize;
}
@ -59,13 +59,13 @@ pub trait ColumnSelectorThree {
type SECOND: Decompress;
/// Third desired column value
type THIRD: Decompress;
/// Mask to obtain desired values, should correspond to the order of columns in a snapshot.
/// Mask to obtain desired values, should correspond to the order of columns in a static_file.
const MASK: usize;
}
#[macro_export]
/// Add mask to select `N` column values from a specific snapshot segment row.
macro_rules! add_snapshot_mask {
/// Add mask to select `N` column values from a specific static file segment row.
macro_rules! add_static_file_mask {
($mask_struct:tt, $type1:ty, $mask:expr) => {
impl ColumnSelectorOne for $mask_struct<$type1> {
type FIRST = $type1;
@ -80,7 +80,7 @@ macro_rules! add_snapshot_mask {
}
};
($mask_struct:tt, $type1:ty, $type2:ty, $type3:ty, $mask:expr) => {
impl ColumnSelectorTwo for $mask_struct<$type1, $type2, $type3> {
impl ColumnSelectorThree for $mask_struct<$type1, $type2, $type3> {
type FIRST = $type1;
type SECOND = $type2;
type THIRD = $type3;

View File

@ -0,0 +1,21 @@
use super::{ReceiptMask, TransactionMask};
use crate::{
add_static_file_mask,
static_file::mask::{ColumnSelectorOne, ColumnSelectorTwo, HeaderMask},
table::Table,
HeaderTerminalDifficulties, Receipts, Transactions,
};
use reth_primitives::{BlockHash, Header};
// HEADER MASKS
add_static_file_mask!(HeaderMask, Header, 0b001);
add_static_file_mask!(HeaderMask, <HeaderTerminalDifficulties as Table>::Value, 0b010);
add_static_file_mask!(HeaderMask, BlockHash, 0b100);
add_static_file_mask!(HeaderMask, Header, BlockHash, 0b101);
add_static_file_mask!(HeaderMask, <HeaderTerminalDifficulties as Table>::Value, BlockHash, 0b110);
// RECEIPT MASKS
add_static_file_mask!(ReceiptMask, <Receipts as Table>::Value, 0b1);
// TRANSACTION MASKS
add_static_file_mask!(TransactionMask, <Transactions as Table>::Value, 0b1);

View File

@ -0,0 +1,76 @@
//! reth's static file database table import and access
mod generation;
use std::{
collections::{hash_map::Entry, HashMap},
path::Path,
};
pub use generation::*;
mod cursor;
pub use cursor::StaticFileCursor;
mod mask;
pub use mask::*;
use reth_nippy_jar::{NippyJar, NippyJarError};
use reth_primitives::{
static_file::{SegmentHeader, SegmentRangeInclusive},
StaticFileSegment,
};
mod masks;
/// Alias type for a map of [`StaticFileSegment`] and sorted lists of existing static file ranges.
type SortedStaticFiles =
HashMap<StaticFileSegment, Vec<(SegmentRangeInclusive, Option<SegmentRangeInclusive>)>>;
/// Given the static_files directory path, it returns a list over the existing static_files
/// organized by [`StaticFileSegment`]. Each segment has a sorted list of block ranges and
/// transaction ranges as presented in the file configuration.
pub fn iter_static_files(path: impl AsRef<Path>) -> Result<SortedStaticFiles, NippyJarError> {
let path = path.as_ref();
if !path.exists() {
reth_primitives::fs::create_dir_all(path)
.map_err(|err| NippyJarError::Custom(err.to_string()))?;
}
let mut static_files = SortedStaticFiles::default();
let entries = reth_primitives::fs::read_dir(path)
.map_err(|err| NippyJarError::Custom(err.to_string()))?
.filter_map(Result::ok)
.collect::<Vec<_>>();
for entry in entries {
if entry.metadata().map_or(false, |metadata| metadata.is_file()) {
if let Some((segment, _)) =
StaticFileSegment::parse_filename(&entry.file_name().to_string_lossy())
{
let jar = NippyJar::<SegmentHeader>::load(&entry.path())?;
let (block_range, tx_range) = (
jar.user_header().block_range().copied(),
jar.user_header().tx_range().copied(),
);
if let Some(block_range) = block_range {
match static_files.entry(segment) {
Entry::Occupied(mut entry) => {
entry.get_mut().push((block_range, tx_range));
}
Entry::Vacant(entry) => {
entry.insert(vec![(block_range, tx_range)]);
}
}
}
}
}
}
for (_, range_list) in static_files.iter_mut() {
// Sort by block end range.
range_list.sort_by(|a, b| a.0.end().cmp(&b.0.end()));
}
Ok(static_files)
}

View File

@ -29,6 +29,7 @@ macro_rules! impl_compression_for_compact {
}
impl_compression_for_compact!(
SealedHeader,
Header,
Account,
Log,

View File

@ -246,7 +246,7 @@ tables! {
table CanonicalHeaders<Key = BlockNumber, Value = HeaderHash>;
/// Stores the total difficulty from a block header.
table HeaderTD<Key = BlockNumber, Value = CompactU256>;
table HeaderTerminalDifficulties<Key = BlockNumber, Value = CompactU256>;
/// Stores the block number corresponding to a header.
table HeaderNumbers<Key = BlockHash, Value = BlockNumber>;
@ -269,12 +269,12 @@ tables! {
table Transactions<Key = TxNumber, Value = TransactionSignedNoHash>;
/// Stores the mapping of the transaction hash to the transaction number.
table TxHashNumber<Key = TxHash, Value = TxNumber>;
table TransactionHashNumbers<Key = TxHash, Value = TxNumber>;
/// Stores the mapping of transaction number to the blocks number.
///
/// The key is the highest transaction ID in the block.
table TransactionBlock<Key = TxNumber, Value = BlockNumber>;
table TransactionBlocks<Key = TxNumber, Value = BlockNumber>;
/// Canonical only Stores transaction receipts.
table Receipts<Key = TxNumber, Value = Receipt>;
@ -309,7 +309,7 @@ tables! {
/// * If there were no shard we would get `None` entry or entry of different storage key.
///
/// Code example can be found in `reth_provider::HistoricalStateProviderRef`
table AccountHistory<Key = ShardedKey<Address>, Value = BlockNumberList>;
table AccountsHistory<Key = ShardedKey<Address>, Value = BlockNumberList>;
/// Stores pointers to block number changeset with changes for each storage key.
///
@ -329,29 +329,29 @@ tables! {
/// * If there were no shard we would get `None` entry or entry of different storage key.
///
/// Code example can be found in `reth_provider::HistoricalStateProviderRef`
table StorageHistory<Key = StorageShardedKey, Value = BlockNumberList>;
table StoragesHistory<Key = StorageShardedKey, Value = BlockNumberList>;
/// Stores the state of an account before a certain transaction changed it.
/// Change on state can be: account is created, selfdestructed, touched while empty
/// or changed balance,nonce.
table AccountChangeSet<Key = BlockNumber, Value = AccountBeforeTx, SubKey = Address>;
table AccountChangeSets<Key = BlockNumber, Value = AccountBeforeTx, SubKey = Address>;
/// Stores the state of a storage key before a certain transaction changed it.
/// If [`StorageEntry::value`] is zero, this means storage was not existing
/// and needs to be removed.
table StorageChangeSet<Key = BlockNumberAddress, Value = StorageEntry, SubKey = B256>;
table StorageChangeSets<Key = BlockNumberAddress, Value = StorageEntry, SubKey = B256>;
/// Stores the current state of an [`Account`] indexed with `keccak256Address`
/// This table is in preparation for merkelization and calculation of state root.
/// We are saving whole account data as it is needed for partial update when
/// part of storage is changed. Benefit for merkelization is that hashed addresses are sorted.
table HashedAccount<Key = B256, Value = Account>;
table HashedAccounts<Key = B256, Value = Account>;
/// Stores the current storage values indexed with `keccak256Address` and
/// hash of storage key `keccak256key`.
/// This table is in preparation for merkelization and calculation of state root.
/// Benefit for merklization is that hashed addresses/keys are sorted.
table HashedStorage<Key = B256, Value = StorageEntry, SubKey = B256>;
table HashedStorages<Key = B256, Value = StorageEntry, SubKey = B256>;
/// Stores the current state's Merkle Patricia Tree.
table AccountsTrie<Key = StoredNibbles, Value = StoredBranchNode>;
@ -362,13 +362,13 @@ tables! {
/// Stores the transaction sender for each canonical transaction.
/// It is needed to speed up execution stage and allows fetching signer without doing
/// transaction signed recovery
table TxSenders<Key = TxNumber, Value = Address>;
table TransactionSenders<Key = TxNumber, Value = Address>;
/// Stores the highest synced block number and stage-specific checkpoint of each stage.
table SyncStage<Key = StageId, Value = StageCheckpoint>;
table StageCheckpoints<Key = StageId, Value = StageCheckpoint>;
/// Stores arbitrary data to keep track of a stage first-sync progress.
table SyncStageProgress<Key = StageId, Value = Vec<u8>>;
table StageCheckpointProgresses<Key = StageId, Value = Vec<u8>>;
/// Stores the highest pruned block number and prune mode of each prune segment.
table PruneCheckpoints<Key = PruneSegment, Value = PruneCheckpoint>;

View File

@ -11,7 +11,7 @@ use reth_codecs::{derive_arbitrary, Compact};
use reth_primitives::{Account, Address, BlockNumber, Buf};
use serde::{Deserialize, Serialize};
/// Account as it is saved inside [`AccountChangeSet`][crate::tables::AccountChangeSet].
/// Account as it is saved inside [`AccountChangeSets`][crate::tables::AccountChangeSets].
///
/// [`Address`] is the subkey.
#[derive_arbitrary(compact)]
@ -57,7 +57,7 @@ impl Compact for AccountBeforeTx {
}
/// [`BlockNumber`] concatenated with [`Address`]. Used as the key for
/// [`StorageChangeSet`](crate::tables::StorageChangeSet)
/// [`StorageChangeSets`](crate::tables::StorageChangeSets)
///
/// Since it's used as a key, it isn't compressed when encoding it.
#[derive(

View File

@ -53,6 +53,12 @@ impl<K: Key> RawKey<K> {
Self { key: K::encode(key).into(), _phantom: std::marker::PhantomData }
}
/// Creates a raw key from an existing `Vec`. Useful when we already have the encoded
/// key.
pub fn from_vec(vec: Vec<u8>) -> Self {
Self { key: vec, _phantom: std::marker::PhantomData }
}
/// Returns the decoded value.
pub fn key(&self) -> Result<K, DatabaseError> {
K::decode(&self.key)
@ -112,6 +118,12 @@ impl<V: Value> RawValue<V> {
Self { value: V::compress(value).into(), _phantom: std::marker::PhantomData }
}
/// Creates a raw value from an existing `Vec`. Useful when we already have the encoded
/// value.
pub fn from_vec(vec: Vec<u8>) -> Self {
Self { value: vec, _phantom: std::marker::PhantomData }
}
/// Returns the decompressed value.
pub fn value(&self) -> Result<V, DatabaseError> {
V::decompress(&self.value)

View File

@ -8,8 +8,8 @@ use std::{
/// The name of the file that contains the version of the database.
pub const DB_VERSION_FILE_NAME: &str = "database.version";
/// The version of the database stored in the [DB_VERSION_FILE_NAME] file in the same directory as
/// database. Example: `1`.
pub const DB_VERSION: u64 = 1;
/// database.
pub const DB_VERSION: u64 = 2;
/// Error when checking a database version using [check_db_version_file]
#[derive(thiserror::Error, Debug)]

View File

@ -17,13 +17,13 @@ name = "reth_libmdbx"
[dependencies]
bitflags.workspace = true
byteorder = "1"
derive_more = "0.99"
derive_more.workspace = true
indexmap = "2"
libc = "0.2"
parking_lot.workspace = true
thiserror.workspace = true
dashmap = { version = "5.5.3", features = ["inline"], optional = true }
tracing = { workspace = true, optional = true }
tracing.workspace = true
ffi = { package = "reth-mdbx-sys", path = "./mdbx-sys" }
@ -33,7 +33,7 @@ libffi = "3.2.0"
[features]
default = []
return-borrowed = []
read-tx-timeouts = ["dashmap", "dashmap/inline", "tracing"]
read-tx-timeouts = ["dashmap", "dashmap/inline"]
[dev-dependencies]
pprof = { workspace = true, features = ["flamegraph", "frame-pointer", "criterion"] }

View File

@ -20,6 +20,7 @@ use std::{
thread::sleep,
time::Duration,
};
use tracing::warn;
/// The default maximum duration of a read transaction.
#[cfg(feature = "read-tx-timeouts")]
@ -96,6 +97,7 @@ impl Environment {
/// Create a read-write transaction for use with the environment. This method will block while
/// there are any other read-write transactions open on the environment.
pub fn begin_rw_txn(&self) -> Result<Transaction<RW>> {
let mut warned = false;
let txn = loop {
let (tx, rx) = sync_channel(0);
self.txn_manager().send_message(TxnManagerMessage::Begin {
@ -105,6 +107,10 @@ impl Environment {
});
let res = rx.recv().unwrap();
if let Err(Error::Busy) = &res {
if !warned {
warned = true;
warn!(target: "libmdbx", "Process stalled, awaiting read-write transaction lock.");
}
sleep(Duration::from_millis(250));
continue
}
@ -937,7 +943,8 @@ mod tests {
.open(tempdir.path())
.unwrap();
// Insert some data in the database, so the read transaction can lock on the snapshot of it
// Insert some data in the database, so the read transaction can lock on the static file of
// it
{
let tx = env.begin_rw_txn().unwrap();
let db = tx.open_db(None).unwrap();
@ -950,7 +957,8 @@ mod tests {
// Create a read transaction
let _tx_ro = env.begin_ro_txn().unwrap();
// Change previously inserted data, so the read transaction would use the previous snapshot
// Change previously inserted data, so the read transaction would use the previous static
// file
{
let tx = env.begin_rw_txn().unwrap();
let db = tx.open_db(None).unwrap();
@ -961,7 +969,7 @@ mod tests {
}
// Insert more data in the database, so we hit the DB size limit error, and MDBX tries to
// kick long-lived readers and delete their snapshots
// kick long-lived readers and delete their static_files
{
let tx = env.begin_rw_txn().unwrap();
let db = tx.open_db(None).unwrap();

View File

@ -25,7 +25,7 @@ pub enum SyncMode {
///
/// [SyncMode::UtterlyNoSync] the [SyncMode::SafeNoSync] flag disable similarly flush system
/// buffers to disk when committing a transaction. But there is a huge difference in how
/// are recycled the MVCC snapshots corresponding to previous "steady" transactions (see
/// are recycled the MVCC static_files corresponding to previous "steady" transactions (see
/// below).
///
/// With [crate::EnvironmentKind::WriteMap] the [SyncMode::SafeNoSync] instructs MDBX to use

View File

@ -15,6 +15,8 @@ workspace = true
name = "reth_nippy_jar"
[dependencies]
# reth
reth-primitives.workspace = true
# filter
ph = "0.8.0"
@ -33,7 +35,7 @@ serde = { version = "1.0", features = ["derive"] }
tracing = "0.1.0"
anyhow = "1.0"
thiserror.workspace = true
derive_more = "0.99"
derive_more.workspace = true
[dev-dependencies]
rand = { version = "0.8", features = ["small_rng"] }

View File

@ -7,6 +7,8 @@ pub enum NippyJarError {
Internal(#[from] Box<dyn std::error::Error + Send + Sync>),
#[error(transparent)]
Disconnect(#[from] std::io::Error),
#[error(transparent)]
FileSystem(#[from] reth_primitives::fs::FsPathError),
#[error("{0}")]
Custom(String),
#[error(transparent)]

View File

@ -206,6 +206,16 @@ impl<H: NippyJarHeader> NippyJar<H> {
&self.user_header
}
/// Gets total columns in jar.
pub fn columns(&self) -> usize {
self.columns
}
/// Gets total rows in jar.
pub fn rows(&self) -> usize {
self.rows
}
/// Returns the size of inclusion filter
pub fn filter_size(&self) -> usize {
self.size()
@ -232,7 +242,9 @@ impl<H: NippyJarHeader> NippyJar<H> {
/// **The user must ensure the header type matches the one used during the jar's creation.**
pub fn load(path: &Path) -> Result<Self, NippyJarError> {
// Read [`Self`] located at the data file.
let config_file = File::open(path.with_extension(CONFIG_FILE_EXTENSION))?;
let config_path = path.with_extension(CONFIG_FILE_EXTENSION);
let config_file = File::open(&config_path)
.map_err(|err| reth_primitives::fs::FsPathError::open(err, config_path))?;
let mut obj: Self = bincode::deserialize_from(&config_file)?;
obj.path = path.to_path_buf();
@ -269,6 +281,21 @@ impl<H: NippyJarHeader> NippyJar<H> {
self.path.with_extension(CONFIG_FILE_EXTENSION)
}
/// Deletes from disk this [`NippyJar`] alongside every satellite file.
pub fn delete(self) -> Result<(), NippyJarError> {
// TODO(joshie): ensure consistency on unexpected shutdown
for path in
[self.data_path().into(), self.index_path(), self.offsets_path(), self.config_path()]
{
if path.exists() {
reth_primitives::fs::remove_file(path)?;
}
}
Ok(())
}
/// Returns a [`DataReader`] of the data and offset file
pub fn open_data_reader(&self) -> Result<DataReader, NippyJarError> {
DataReader::new(self.data_path())
@ -338,14 +365,17 @@ impl<H: NippyJarHeader> NippyJar<H> {
/// Writes all data and configuration to a file and the offset index to another.
pub fn freeze(
&mut self,
mut self,
columns: Vec<impl IntoIterator<Item = ColumnResult<Vec<u8>>>>,
total_rows: u64,
) -> Result<(), NippyJarError> {
) -> Result<Self, NippyJarError> {
self.check_before_freeze(&columns)?;
debug!(target: "nippy-jar", path=?self.data_path(), "Opening data file.");
// Write phf, filter and offset index to file
self.freeze_filters()?;
// Creates the writer, data and offsets file
let mut writer = NippyJarWriter::new(self)?;
@ -355,12 +385,9 @@ impl<H: NippyJarHeader> NippyJar<H> {
// Flushes configuration and offsets to disk
writer.commit()?;
// Write phf, filter and offset index to file
self.freeze_filters()?;
debug!(target: "nippy-jar", ?writer, "Finished writing data.");
debug!(target: "nippy-jar", jar=?self, "Finished writing data.");
Ok(())
Ok(writer.into_jar())
}
/// Freezes [`PerfectHashingFunction`], [`InclusionFilter`] and the offset index to file.
@ -428,9 +455,9 @@ impl<H: NippyJarHeader> PerfectHashingFunction for NippyJar<H> {
}
}
/// Manages the reading of snapshot data using memory-mapped files.
/// Manages the reading of static file data using memory-mapped files.
///
/// Holds file and mmap descriptors of the data and offsets files of a snapshot.
/// Holds file and mmap descriptors of the data and offsets files of a static_file.
#[derive(Debug)]
pub struct DataReader {
/// Data file descriptor. Needs to be kept alive as long as `data_mmap` handle.
@ -558,15 +585,21 @@ mod tests {
let num_rows = col1.len() as u64;
let file_path = tempfile::NamedTempFile::new().unwrap();
let mut nippy = NippyJar::new_without_header(num_columns, file_path.path());
assert!(matches!(NippyJar::set_keys(&mut nippy, &col1), Err(NippyJarError::PHFMissing)));
let check_phf = |nippy: &mut NippyJar<_>| {
let create_nippy = || -> NippyJar<()> {
let mut nippy = NippyJar::new_without_header(num_columns, file_path.path());
assert!(matches!(
NippyJar::get_index(nippy, &col1[0]),
NippyJar::set_keys(&mut nippy, &col1),
Err(NippyJarError::PHFMissing)
));
nippy
};
let check_phf = |mut nippy: NippyJar<_>| {
assert!(matches!(
NippyJar::get_index(&nippy, &col1[0]),
Err(NippyJarError::PHFMissingKeys)
));
assert!(NippyJar::set_keys(nippy, &col1).is_ok());
assert!(NippyJar::set_keys(&mut nippy, &col1).is_ok());
let collect_indexes = |nippy: &NippyJar<_>| -> Vec<u64> {
col1.iter()
@ -575,12 +608,12 @@ mod tests {
};
// Ensure all indexes are unique
let indexes = collect_indexes(nippy);
let indexes = collect_indexes(&nippy);
assert_eq!(indexes.iter().collect::<HashSet<_>>().len(), indexes.len());
// Ensure reproducibility
assert!(NippyJar::set_keys(nippy, &col1).is_ok());
assert_eq!(indexes, collect_indexes(nippy));
assert!(NippyJar::set_keys(&mut nippy, &col1).is_ok());
assert_eq!(indexes, collect_indexes(&nippy));
// Ensure that loaded phf provides the same function outputs
nippy.prepare_index(clone_with_result(&col1), col1.len()).unwrap();
@ -593,12 +626,10 @@ mod tests {
};
// fmph bytes size for 100 values of 32 bytes: 54
nippy = nippy.with_fmph();
check_phf(&mut nippy);
check_phf(create_nippy().with_fmph());
// fmph bytes size for 100 values of 32 bytes: 46
nippy = nippy.with_gofmph();
check_phf(&mut nippy);
check_phf(create_nippy().with_gofmph());
}
#[test]
@ -631,7 +662,9 @@ mod tests {
assert!(InclusionFilter::add(&mut nippy, &col1[2]).is_ok());
assert!(InclusionFilter::add(&mut nippy, &col1[3]).is_ok());
nippy.freeze(vec![clone_with_result(&col1), clone_with_result(&col2)], num_rows).unwrap();
let nippy = nippy
.freeze(vec![clone_with_result(&col1), clone_with_result(&col2)], num_rows)
.unwrap();
let mut loaded_nippy = NippyJar::load_without_header(file_path.path()).unwrap();
loaded_nippy.load_filters().unwrap();
@ -675,6 +708,10 @@ mod tests {
Err(NippyJarError::CompressorNotReady)
));
let mut nippy =
NippyJar::new_without_header(num_columns, file_path.path()).with_zstd(true, 5000);
assert!(nippy.compressor().is_some());
nippy.prepare_compression(vec![col1.clone(), col2.clone()]).unwrap();
if let Some(Compressors::Zstd(zstd)) = &nippy.compressor() {
@ -684,7 +721,9 @@ mod tests {
));
}
nippy.freeze(vec![clone_with_result(&col1), clone_with_result(&col2)], num_rows).unwrap();
let nippy = nippy
.freeze(vec![clone_with_result(&col1), clone_with_result(&col2)], num_rows)
.unwrap();
let mut loaded_nippy = NippyJar::load_without_header(file_path.path()).unwrap();
loaded_nippy.load_filters().unwrap();
@ -724,10 +763,12 @@ mod tests {
let nippy = NippyJar::new_without_header(num_columns, file_path.path());
assert!(nippy.compressor().is_none());
let mut nippy = NippyJar::new_without_header(num_columns, file_path.path()).with_lz4();
let nippy = NippyJar::new_without_header(num_columns, file_path.path()).with_lz4();
assert!(nippy.compressor().is_some());
nippy.freeze(vec![clone_with_result(&col1), clone_with_result(&col2)], num_rows).unwrap();
let nippy = nippy
.freeze(vec![clone_with_result(&col1), clone_with_result(&col2)], num_rows)
.unwrap();
let mut loaded_nippy = NippyJar::load_without_header(file_path.path()).unwrap();
loaded_nippy.load_filters().unwrap();
@ -760,11 +801,13 @@ mod tests {
let nippy = NippyJar::new_without_header(num_columns, file_path.path());
assert!(nippy.compressor().is_none());
let mut nippy =
let nippy =
NippyJar::new_without_header(num_columns, file_path.path()).with_zstd(false, 5000);
assert!(nippy.compressor().is_some());
nippy.freeze(vec![clone_with_result(&col1), clone_with_result(&col2)], num_rows).unwrap();
let nippy = nippy
.freeze(vec![clone_with_result(&col1), clone_with_result(&col2)], num_rows)
.unwrap();
let mut loaded_nippy = NippyJar::load_without_header(file_path.path()).unwrap();
loaded_nippy.load_filters().unwrap();
@ -903,7 +946,7 @@ mod tests {
let mut data = col1.iter().zip(col2.iter()).enumerate().collect::<Vec<_>>();
data.shuffle(&mut rand::thread_rng());
// Imagine `Blocks` snapshot file has two columns: `Block | StoredWithdrawals`
// Imagine `Blocks` static file has two columns: `Block | StoredWithdrawals`
const BLOCKS_FULL_MASK: usize = 0b11;
// Read both columns
@ -1047,7 +1090,7 @@ mod tests {
col1: &[Vec<u8>],
col2: &[Vec<u8>],
) {
let mut nippy = NippyJar::load_without_header(file_path).unwrap();
let nippy = NippyJar::load_without_header(file_path).unwrap();
// Set the baseline that should be unwinded to
let initial_rows = nippy.rows;
@ -1059,7 +1102,7 @@ mod tests {
assert!(initial_offset_size > 0);
// Appends a third row
let mut writer = NippyJarWriter::new(&mut nippy).unwrap();
let mut writer = NippyJarWriter::new(nippy).unwrap();
writer.append_column(Some(Ok(&col1[2]))).unwrap();
writer.append_column(Some(Ok(&col2[2]))).unwrap();
@ -1073,7 +1116,7 @@ mod tests {
// Simulate an unexpected shutdown of the writer, before it can finish commit()
drop(writer);
let mut nippy = NippyJar::load_without_header(file_path).unwrap();
let nippy = NippyJar::load_without_header(file_path).unwrap();
assert_eq!(initial_rows, nippy.rows);
// Data was written successfuly
@ -1090,21 +1133,20 @@ mod tests {
// Writer will execute a consistency check and verify first that the offset list on disk
// doesn't match the nippy.rows, and prune it. Then, it will prune the data file
// accordingly as well.
let _writer = NippyJarWriter::new(&mut nippy).unwrap();
assert_eq!(initial_rows, nippy.rows);
let writer = NippyJarWriter::new(nippy).unwrap();
assert_eq!(initial_rows, writer.rows());
assert_eq!(
initial_offset_size,
File::open(nippy.offsets_path()).unwrap().metadata().unwrap().len() as usize
File::open(writer.offsets_path()).unwrap().metadata().unwrap().len() as usize
);
assert_eq!(
initial_data_size,
File::open(nippy.data_path()).unwrap().metadata().unwrap().len() as usize
File::open(writer.data_path()).unwrap().metadata().unwrap().len() as usize
);
assert_eq!(initial_rows, nippy.rows);
}
fn test_append_consistency_no_commit(file_path: &Path, col1: &[Vec<u8>], col2: &[Vec<u8>]) {
let mut nippy = NippyJar::load_without_header(file_path).unwrap();
let nippy = NippyJar::load_without_header(file_path).unwrap();
// Set the baseline that should be unwinded to
let initial_rows = nippy.rows;
@ -1117,14 +1159,14 @@ mod tests {
// Appends a third row, so we have an offset list in memory, which is not flushed to disk,
// while the data has been.
let mut writer = NippyJarWriter::new(&mut nippy).unwrap();
let mut writer = NippyJarWriter::new(nippy).unwrap();
writer.append_column(Some(Ok(&col1[2]))).unwrap();
writer.append_column(Some(Ok(&col2[2]))).unwrap();
// Simulate an unexpected shutdown of the writer, before it can call commit()
drop(writer);
let mut nippy = NippyJar::load_without_header(file_path).unwrap();
let nippy = NippyJar::load_without_header(file_path).unwrap();
assert_eq!(initial_rows, nippy.rows);
// Data was written successfuly
@ -1140,13 +1182,12 @@ mod tests {
// Writer will execute a consistency check and verify that the data file has more data than
// it should, and resets it to the last offset of the list (on disk here)
let _writer = NippyJarWriter::new(&mut nippy).unwrap();
assert_eq!(initial_rows, nippy.rows);
let writer = NippyJarWriter::new(nippy).unwrap();
assert_eq!(initial_rows, writer.rows());
assert_eq!(
initial_data_size,
File::open(nippy.data_path()).unwrap().metadata().unwrap().len() as usize
File::open(writer.data_path()).unwrap().metadata().unwrap().len() as usize
);
assert_eq!(initial_rows, nippy.rows);
}
fn append_two_rows(num_columns: usize, file_path: &Path, col1: &[Vec<u8>], col2: &[Vec<u8>]) {
@ -1157,7 +1198,7 @@ mod tests {
assert_eq!(nippy.max_row_size, 0);
assert_eq!(nippy.rows, 0);
let mut writer = NippyJarWriter::new(&mut nippy).unwrap();
let mut writer = NippyJarWriter::new(nippy).unwrap();
assert_eq!(writer.column(), 0);
writer.append_column(Some(Ok(&col1[0]))).unwrap();
@ -1173,26 +1214,26 @@ mod tests {
let expected_data_file_size = *writer.offsets().last().unwrap();
writer.commit().unwrap();
assert_eq!(nippy.max_row_size, col1[0].len() + col2[0].len());
assert_eq!(nippy.rows, 1);
assert_eq!(writer.max_row_size(), col1[0].len() + col2[0].len());
assert_eq!(writer.rows(), 1);
assert_eq!(
File::open(nippy.offsets_path()).unwrap().metadata().unwrap().len(),
File::open(writer.offsets_path()).unwrap().metadata().unwrap().len(),
1 + num_columns as u64 * 8 + 8
);
assert_eq!(
File::open(nippy.data_path()).unwrap().metadata().unwrap().len(),
File::open(writer.data_path()).unwrap().metadata().unwrap().len(),
expected_data_file_size
);
}
// Load and add 1 row
{
let mut nippy = NippyJar::load_without_header(file_path).unwrap();
let nippy = NippyJar::load_without_header(file_path).unwrap();
// Check if it was committed successfuly
assert_eq!(nippy.max_row_size, col1[0].len() + col2[0].len());
assert_eq!(nippy.rows, 1);
let mut writer = NippyJarWriter::new(&mut nippy).unwrap();
let mut writer = NippyJarWriter::new(nippy).unwrap();
assert_eq!(writer.column(), 0);
writer.append_column(Some(Ok(&col1[1]))).unwrap();
@ -1208,22 +1249,22 @@ mod tests {
let expected_data_file_size = *writer.offsets().last().unwrap();
writer.commit().unwrap();
assert_eq!(nippy.max_row_size, col1[0].len() + col2[0].len());
assert_eq!(nippy.rows, 2);
assert_eq!(writer.max_row_size(), col1[0].len() + col2[0].len());
assert_eq!(writer.rows(), 2);
assert_eq!(
File::open(nippy.offsets_path()).unwrap().metadata().unwrap().len(),
1 + nippy.rows as u64 * num_columns as u64 * 8 + 8
File::open(writer.offsets_path()).unwrap().metadata().unwrap().len(),
1 + writer.rows() as u64 * num_columns as u64 * 8 + 8
);
assert_eq!(
File::open(nippy.data_path()).unwrap().metadata().unwrap().len(),
File::open(writer.data_path()).unwrap().metadata().unwrap().len(),
expected_data_file_size
);
}
}
fn prune_rows(num_columns: usize, file_path: &Path, col1: &[Vec<u8>], col2: &[Vec<u8>]) {
let mut nippy = NippyJar::load_without_header(file_path).unwrap();
let mut writer = NippyJarWriter::new(&mut nippy).unwrap();
let nippy = NippyJar::load_without_header(file_path).unwrap();
let mut writer = NippyJarWriter::new(nippy).unwrap();
// Appends a third row, so we have an offset list in memory, which is not flushed to disk
writer.append_column(Some(Ok(&col1[2]))).unwrap();
@ -1231,32 +1272,38 @@ mod tests {
// This should prune from the on-memory offset list and ondisk offset list
writer.prune_rows(2).unwrap();
assert_eq!(nippy.rows, 1);
assert_eq!(writer.rows(), 1);
assert_eq!(
File::open(nippy.offsets_path()).unwrap().metadata().unwrap().len(),
1 + nippy.rows as u64 * num_columns as u64 * 8 + 8
File::open(writer.offsets_path()).unwrap().metadata().unwrap().len(),
1 + writer.rows() as u64 * num_columns as u64 * 8 + 8
);
let expected_data_size = col1[0].len() + col2[0].len();
assert_eq!(
File::open(nippy.data_path()).unwrap().metadata().unwrap().len() as usize,
File::open(writer.data_path()).unwrap().metadata().unwrap().len() as usize,
expected_data_size
);
let data_reader = nippy.open_data_reader().unwrap();
// there are only two valid offsets. so index 2 actually represents the expected file
// data size.
assert_eq!(data_reader.offset(2), expected_data_size as u64);
let nippy = NippyJar::load_without_header(file_path).unwrap();
{
let data_reader = nippy.open_data_reader().unwrap();
// there are only two valid offsets. so index 2 actually represents the expected file
// data size.
assert_eq!(data_reader.offset(2), expected_data_size as u64);
}
// This should prune from the ondisk offset list and clear the jar.
let mut writer = NippyJarWriter::new(&mut nippy).unwrap();
let mut writer = NippyJarWriter::new(nippy).unwrap();
writer.prune_rows(1).unwrap();
assert_eq!(nippy.rows, 0);
assert_eq!(nippy.max_row_size, 0);
assert_eq!(File::open(nippy.data_path()).unwrap().metadata().unwrap().len() as usize, 0);
assert_eq!(writer.rows(), 0);
assert_eq!(writer.max_row_size(), 0);
assert_eq!(File::open(writer.data_path()).unwrap().metadata().unwrap().len() as usize, 0);
// Only the byte that indicates how many bytes per offset should be left
assert_eq!(File::open(nippy.offsets_path()).unwrap().metadata().unwrap().len() as usize, 1);
assert_eq!(
File::open(writer.offsets_path()).unwrap().metadata().unwrap().len() as usize,
1
);
}
fn simulate_interrupted_prune(
@ -1265,7 +1312,7 @@ mod tests {
num_rows: u64,
missing_offsets: u64,
) {
let mut nippy = NippyJar::load_without_header(file_path).unwrap();
let nippy = NippyJar::load_without_header(file_path).unwrap();
let reader = nippy.open_data_reader().unwrap();
let offsets_file =
OpenOptions::new().read(true).write(true).open(nippy.offsets_path()).unwrap();
@ -1284,6 +1331,6 @@ mod tests {
data_file.set_len(data_len - 32 * missing_offsets).unwrap();
// runs the consistency check.
let _ = NippyJarWriter::new(&mut nippy).unwrap();
let _ = NippyJarWriter::new(nippy).unwrap();
}
}

View File

@ -1,9 +1,8 @@
use crate::{compression::Compression, ColumnResult, NippyJar, NippyJarError, NippyJarHeader};
use std::{
cmp::Ordering,
fmt,
fs::{File, OpenOptions},
io::{Read, Seek, SeekFrom, Write},
io::{BufWriter, Read, Seek, SeekFrom, Write},
path::Path,
};
@ -23,14 +22,15 @@ const OFFSET_SIZE_BYTES: u64 = 8;
///
/// ## Data file layout
/// The data file is represented just as a sequence of bytes of data without any delimiters
pub struct NippyJarWriter<'a, H> {
/// Reference to the associated [`NippyJar`], containing all necessary configurations for data
#[derive(Debug)]
pub struct NippyJarWriter<H: NippyJarHeader = ()> {
/// Associated [`NippyJar`], containing all necessary configurations for data
/// handling.
jar: &'a mut NippyJar<H>,
jar: NippyJar<H>,
/// File handle to where the data is stored.
data_file: File,
data_file: BufWriter<File>,
/// File handle to where the offsets are stored.
offsets_file: File,
offsets_file: BufWriter<File>,
/// Temporary buffer to reuse when compressing data.
tmp_buf: Vec<u8>,
/// Used to find the maximum uncompressed size of a row in a jar.
@ -41,21 +41,19 @@ pub struct NippyJarWriter<'a, H> {
column: usize,
}
impl<H> fmt::Debug for NippyJarWriter<'_, H> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("NippyJarWriter").finish_non_exhaustive()
}
}
impl<'a, H: NippyJarHeader> NippyJarWriter<'a, H> {
pub fn new(jar: &'a mut NippyJar<H>) -> Result<Self, NippyJarError> {
impl<H: NippyJarHeader> NippyJarWriter<H> {
/// Creates a [`NippyJarWriter`] from [`NippyJar`].
pub fn new(mut jar: NippyJar<H>) -> Result<Self, NippyJarError> {
let (data_file, offsets_file, is_created) =
Self::create_or_open_files(jar.data_path(), &jar.offsets_path())?;
// Makes sure we don't have dangling data and offset files
jar.freeze_config()?;
let mut writer = Self {
jar,
data_file,
offsets_file,
data_file: BufWriter::new(data_file),
offsets_file: BufWriter::new(offsets_file),
tmp_buf: Vec::with_capacity(1_000_000),
uncompressed_row_size: 0,
offsets: Vec::with_capacity(1_000_000),
@ -66,35 +64,56 @@ impl<'a, H: NippyJarHeader> NippyJarWriter<'a, H> {
// changes if necessary.
if !is_created {
writer.check_consistency_and_heal()?;
writer.commit()?;
}
Ok(writer)
}
/// Returns a reference to `H` of [`NippyJar`]
pub fn user_header(&self) -> &H {
&self.jar.user_header
}
/// Returns a mutable reference to `H` of [`NippyJar`]
pub fn user_header_mut(&mut self) -> &mut H {
&mut self.jar.user_header
}
/// Gets total writter rows in jar.
pub fn rows(&self) -> usize {
self.jar.rows()
}
/// Consumes the writer and returns the associated [`NippyJar`].
pub fn into_jar(self) -> NippyJar<H> {
self.jar
}
fn create_or_open_files(
data: &Path,
offsets: &Path,
) -> Result<(File, File, bool), NippyJarError> {
let is_created = !data.exists() || !offsets.exists();
let mut data_file = if !data.exists() {
File::create(data)?
} else {
OpenOptions::new().read(true).write(true).open(data)?
};
if !data.exists() {
// File::create is write-only (no reading possible)
File::create(data)?;
}
let mut data_file = OpenOptions::new().read(true).write(true).open(data)?;
data_file.seek(SeekFrom::End(0))?;
let mut offsets_file = if !offsets.exists() {
let mut offsets = File::create(offsets)?;
if !offsets.exists() {
// File::create is write-only (no reading possible)
File::create(offsets)?;
}
// First byte of the offset file is the size of one offset in bytes
offsets.write_all(&[OFFSET_SIZE_BYTES as u8])?;
offsets.sync_all()?;
let mut offsets_file = OpenOptions::new().read(true).write(true).open(offsets)?;
offsets
} else {
OpenOptions::new().read(true).write(true).open(offsets)?
};
// First byte of the offset file is the size of one offset in bytes
offsets_file.write_all(&[OFFSET_SIZE_BYTES as u8])?;
offsets_file.sync_all()?;
offsets_file.seek(SeekFrom::End(0))?;
Ok((data_file, offsets_file, is_created))
@ -118,7 +137,7 @@ impl<'a, H: NippyJarHeader> NippyJarWriter<'a, H> {
let expected_offsets_file_size = 1 + // first byte is the size of one offset
OFFSET_SIZE_BYTES * self.jar.rows as u64 * self.jar.columns as u64 + // `offset size * num rows * num columns`
OFFSET_SIZE_BYTES; // expected size of the data file
let actual_offsets_file_size = self.offsets_file.metadata()?.len();
let actual_offsets_file_size = self.offsets_file.get_ref().metadata()?.len();
// Offsets configuration wasn't properly committed
match expected_offsets_file_size.cmp(&actual_offsets_file_size) {
@ -126,7 +145,7 @@ impl<'a, H: NippyJarHeader> NippyJarWriter<'a, H> {
// Happened during an appending job
// TODO: ideally we could truncate until the last offset of the last column of the
// last row inserted
self.offsets_file.set_len(expected_offsets_file_size)?;
self.offsets_file.get_mut().set_len(expected_offsets_file_size)?;
}
Ordering::Greater => {
// Happened during a pruning job
@ -145,14 +164,14 @@ impl<'a, H: NippyJarHeader> NippyJarWriter<'a, H> {
// last offset should match the data_file_len
let last_offset = reader.reverse_offset(0)?;
let data_file_len = self.data_file.metadata()?.len();
let data_file_len = self.data_file.get_ref().metadata()?.len();
// Offset list wasn't properly committed
match last_offset.cmp(&data_file_len) {
Ordering::Less => {
// Happened during an appending job, so we need to truncate the data, since there's
// no way to recover it.
self.data_file.set_len(last_offset)?;
self.data_file.get_mut().set_len(last_offset)?;
}
Ordering::Greater => {
// Happened during a pruning job, so we need to reverse iterate offsets until we
@ -160,12 +179,13 @@ impl<'a, H: NippyJarHeader> NippyJarWriter<'a, H> {
for index in 0..reader.offsets_count()? {
let offset = reader.reverse_offset(index + 1)?;
if offset == data_file_len {
self.offsets_file.set_len(
self.offsets_file
.metadata()?
.len()
.saturating_sub(OFFSET_SIZE_BYTES * (index as u64 + 1)),
)?;
let new_len = self
.offsets_file
.get_ref()
.metadata()?
.len()
.saturating_sub(OFFSET_SIZE_BYTES * (index as u64 + 1));
self.offsets_file.get_mut().set_len(new_len)?;
drop(reader);
@ -229,11 +249,11 @@ impl<'a, H: NippyJarHeader> NippyJarWriter<'a, H> {
self.offsets.push(self.data_file.stream_position()?);
}
self.write_column(value.as_ref())?;
let written = self.write_column(value.as_ref())?;
// Last offset represents the size of the data file if no more data is to be
// appended. Otherwise, represents the offset of the next data item.
self.offsets.push(self.data_file.stream_position()?);
self.offsets.push(self.offsets.last().expect("qed") + written as u64);
}
None => {
return Err(NippyJarError::UnexpectedMissingValue(
@ -248,15 +268,17 @@ impl<'a, H: NippyJarHeader> NippyJarWriter<'a, H> {
}
/// Writes column to data file. If it's the last column of the row, call `finalize_row()`
fn write_column(&mut self, value: &[u8]) -> Result<(), NippyJarError> {
fn write_column(&mut self, value: &[u8]) -> Result<usize, NippyJarError> {
self.uncompressed_row_size += value.len();
if let Some(compression) = &self.jar.compressor {
let len = if let Some(compression) = &self.jar.compressor {
let before = self.tmp_buf.len();
let len = compression.compress_to(value, &mut self.tmp_buf)?;
self.data_file.write_all(&self.tmp_buf[before..before + len])?;
len
} else {
self.data_file.write_all(value)?;
}
value.len()
};
self.column += 1;
@ -264,11 +286,14 @@ impl<'a, H: NippyJarHeader> NippyJarWriter<'a, H> {
self.finalize_row();
}
Ok(())
Ok(len)
}
/// Prunes rows from data and offsets file and updates its configuration on disk
pub fn prune_rows(&mut self, num_rows: usize) -> Result<(), NippyJarError> {
self.offsets_file.flush()?;
self.data_file.flush()?;
// Each column of a row is one offset
let num_offsets = num_rows * self.jar.columns;
@ -283,13 +308,13 @@ impl<'a, H: NippyJarHeader> NippyJarWriter<'a, H> {
self.offsets.truncate(self.offsets.len() - offsets_prune_count);
// Truncate the data file to the new length
self.data_file.set_len(new_len)?;
self.data_file.get_mut().set_len(new_len)?;
}
// Prune from on-disk offset list if there are still rows left to prune
if remaining_to_prune > 0 {
// Get the current length of the on-disk offset file
let length = self.offsets_file.metadata()?.len();
let length = self.offsets_file.get_ref().metadata()?.len();
// Handle non-empty offset file
if length > 1 {
@ -308,8 +333,8 @@ impl<'a, H: NippyJarHeader> NippyJarWriter<'a, H> {
// If all rows are to be pruned
if new_num_offsets <= 1 {
// <= 1 because the one offset would actually be the expected file data size
self.offsets_file.set_len(1)?;
self.data_file.set_len(0)?;
self.offsets_file.get_mut().set_len(1)?;
self.data_file.get_mut().set_len(0)?;
} else {
// Calculate the new length for the on-disk offset list
let new_len = 1 + new_num_offsets * OFFSET_SIZE_BYTES;
@ -318,20 +343,20 @@ impl<'a, H: NippyJarHeader> NippyJarWriter<'a, H> {
.seek(SeekFrom::Start(new_len.saturating_sub(OFFSET_SIZE_BYTES)))?;
// Read the last offset value
let mut last_offset = [0u8; OFFSET_SIZE_BYTES as usize];
self.offsets_file.read_exact(&mut last_offset)?;
self.offsets_file.get_ref().read_exact(&mut last_offset)?;
let last_offset = u64::from_le_bytes(last_offset);
// Update the lengths of both the offsets and data files
self.offsets_file.set_len(new_len)?;
self.data_file.set_len(last_offset)?;
self.offsets_file.get_mut().set_len(new_len)?;
self.data_file.get_mut().set_len(last_offset)?;
}
} else {
return Err(NippyJarError::InvalidPruning(0, remaining_to_prune as u64))
}
}
self.offsets_file.sync_all()?;
self.data_file.sync_all()?;
self.offsets_file.get_ref().sync_all()?;
self.data_file.get_ref().sync_all()?;
self.offsets_file.seek(SeekFrom::End(0))?;
self.data_file.seek(SeekFrom::End(0))?;
@ -358,7 +383,8 @@ impl<'a, H: NippyJarHeader> NippyJarWriter<'a, H> {
/// Commits configuration and offsets to disk. It drains the internal offset list.
pub fn commit(&mut self) -> Result<(), NippyJarError> {
self.data_file.sync_all()?;
self.data_file.flush()?;
self.data_file.get_ref().sync_all()?;
self.commit_offsets()?;
@ -374,11 +400,11 @@ impl<'a, H: NippyJarHeader> NippyJarWriter<'a, H> {
// `append_column()` works alongside commit. So we need to skip it.
let mut last_offset_ondisk = None;
if self.offsets_file.metadata()?.len() > 1 {
if self.offsets_file.get_ref().metadata()?.len() > 1 {
self.offsets_file.seek(SeekFrom::End(-(OFFSET_SIZE_BYTES as i64)))?;
let mut buf = [0u8; OFFSET_SIZE_BYTES as usize];
self.offsets_file.read_exact(&mut buf)?;
self.offsets_file.get_ref().read_exact(&mut buf)?;
last_offset_ondisk = Some(u64::from_le_bytes(buf));
}
@ -393,11 +419,17 @@ impl<'a, H: NippyJarHeader> NippyJarWriter<'a, H> {
}
self.offsets_file.write_all(&offset.to_le_bytes())?;
}
self.offsets_file.sync_all()?;
self.offsets_file.flush()?;
self.offsets_file.get_ref().sync_all()?;
Ok(())
}
#[cfg(test)]
pub fn max_row_size(&self) -> usize {
self.jar.max_row_size
}
#[cfg(test)]
pub fn column(&self) -> usize {
self.column
@ -412,4 +444,14 @@ impl<'a, H: NippyJarHeader> NippyJarWriter<'a, H> {
pub fn offsets_mut(&mut self) -> &mut Vec<u64> {
&mut self.offsets
}
#[cfg(test)]
pub fn offsets_path(&self) -> std::path::PathBuf {
self.jar.offsets_path()
}
#[cfg(test)]
pub fn data_path(&self) -> &Path {
self.jar.data_path()
}
}

View File

@ -18,6 +18,7 @@ reth-interfaces.workspace = true
reth-db.workspace = true
reth-trie.workspace = true
reth-nippy-jar.workspace = true
reth-codecs.workspace = true
reth-node-api.workspace = true
revm.workspace = true

View File

@ -1,15 +1,15 @@
use crate::{StateChanges, StateReverts};
use crate::{providers::StaticFileProviderRWRefMut, StateChanges, StateReverts};
use reth_db::{
cursor::{DbCursorRO, DbCursorRW},
tables,
transaction::{DbTx, DbTxMut},
};
use reth_interfaces::db::DatabaseError;
use reth_interfaces::provider::{ProviderError, ProviderResult};
use reth_primitives::{
logs_bloom,
revm::compat::{into_reth_acc, into_revm_acc},
Account, Address, BlockNumber, Bloom, Bytecode, Log, Receipt, Receipts, StorageEntry, B256,
U256,
Account, Address, BlockNumber, Bloom, Bytecode, Log, Receipt, Receipts, StaticFileSegment,
StorageEntry, B256, U256,
};
use reth_trie::HashedPostState;
use revm::{
@ -285,15 +285,21 @@ impl BundleStateWithReceipts {
std::mem::swap(&mut self.bundle, &mut other)
}
/// Write the [BundleStateWithReceipts] to the database.
/// Write the [BundleStateWithReceipts] to database and receipts to either database or static
/// files if `static_file_producer` is `Some`. It should be none if there is any kind of
/// pruning/filtering over the receipts.
///
/// `is_value_known` should be set to `Not` if the [BundleStateWithReceipts] has some of its
/// state detached, This would make some original values not known.
pub fn write_to_db<TX: DbTxMut + DbTx>(
/// `omit_changed_check` should be set to true of bundle has some of it data
/// detached, This would make some original values not known.
pub fn write_to_storage<TX>(
self,
tx: &TX,
mut static_file_producer: Option<StaticFileProviderRWRefMut<'_>>,
is_value_known: OriginalValuesKnown,
) -> Result<(), DatabaseError> {
) -> ProviderResult<()>
where
TX: DbTxMut + DbTx,
{
let (plain_state, reverts) = self.bundle.into_plain_state_and_reverts(is_value_known);
StateReverts(reverts).write_to_db(tx, self.first_block)?;
@ -303,15 +309,22 @@ impl BundleStateWithReceipts {
let mut receipts_cursor = tx.cursor_write::<tables::Receipts>()?;
for (idx, receipts) in self.receipts.into_iter().enumerate() {
if !receipts.is_empty() {
let block_number = self.first_block + idx as u64;
let (_, body_indices) =
bodies_cursor.seek_exact(block_number)?.unwrap_or_else(|| {
let last_available = bodies_cursor.last().ok().flatten().map(|(number, _)| number);
panic!("body indices for block {block_number} must exist. last available block number: {last_available:?}");
});
let block_number = self.first_block + idx as u64;
let first_tx_index = bodies_cursor
.seek_exact(block_number)?
.map(|(_, indices)| indices.first_tx_num())
.ok_or_else(|| ProviderError::BlockBodyIndicesNotFound(block_number))?;
let first_tx_index = body_indices.first_tx_num();
if let Some(static_file_producer) = &mut static_file_producer {
// Increment block on static file header.
static_file_producer.increment_block(StaticFileSegment::Receipts)?;
for (tx_idx, receipt) in receipts.into_iter().enumerate() {
let receipt = receipt
.expect("receipt should not be filtered when saving to static files.");
static_file_producer.append_receipt(first_tx_index + tx_idx as u64, receipt)?;
}
} else if !receipts.is_empty() {
for (tx_idx, receipt) in receipts.into_iter().enumerate() {
if let Some(receipt) = receipt {
receipts_cursor.append(first_tx_index + tx_idx as u64, receipt)?;
@ -426,7 +439,7 @@ mod tests {
// Check change set
let mut changeset_cursor = provider
.tx_ref()
.cursor_dup_read::<tables::AccountChangeSet>()
.cursor_dup_read::<tables::AccountChangeSets>()
.expect("Could not open changeset cursor");
assert_eq!(
changeset_cursor.seek_exact(1).expect("Could not read account change set"),
@ -549,7 +562,7 @@ mod tests {
state.merge_transitions(BundleRetention::Reverts);
BundleStateWithReceipts::new(state.take_bundle(), Receipts::new(), 1)
.write_to_db(provider.tx_ref(), OriginalValuesKnown::Yes)
.write_to_storage(provider.tx_ref(), None, OriginalValuesKnown::Yes)
.expect("Could not write bundle state to DB");
// Check plain storage state
@ -594,7 +607,7 @@ mod tests {
// Check change set
let mut changeset_cursor = provider
.tx_ref()
.cursor_dup_read::<tables::StorageChangeSet>()
.cursor_dup_read::<tables::StorageChangeSets>()
.expect("Could not open storage changeset cursor");
assert_eq!(
changeset_cursor.seek_exact(BlockNumberAddress((1, address_a))).unwrap(),
@ -647,7 +660,7 @@ mod tests {
state.merge_transitions(BundleRetention::Reverts);
BundleStateWithReceipts::new(state.take_bundle(), Receipts::new(), 2)
.write_to_db(provider.tx_ref(), OriginalValuesKnown::Yes)
.write_to_storage(provider.tx_ref(), None, OriginalValuesKnown::Yes)
.expect("Could not write bundle state to DB");
assert_eq!(
@ -711,7 +724,7 @@ mod tests {
)]));
init_state.merge_transitions(BundleRetention::Reverts);
BundleStateWithReceipts::new(init_state.take_bundle(), Receipts::new(), 0)
.write_to_db(provider.tx_ref(), OriginalValuesKnown::Yes)
.write_to_storage(provider.tx_ref(), None, OriginalValuesKnown::Yes)
.expect("Could not write init bundle state to DB");
let mut state = State::builder().with_bundle_update().build();
@ -856,12 +869,12 @@ mod tests {
let bundle = state.take_bundle();
BundleStateWithReceipts::new(bundle, Receipts::new(), 1)
.write_to_db(provider.tx_ref(), OriginalValuesKnown::Yes)
.write_to_storage(provider.tx_ref(), None, OriginalValuesKnown::Yes)
.expect("Could not write bundle state to DB");
let mut storage_changeset_cursor = provider
.tx_ref()
.cursor_dup_read::<tables::StorageChangeSet>()
.cursor_dup_read::<tables::StorageChangeSets>()
.expect("Could not open plain storage state cursor");
let mut storage_changes = storage_changeset_cursor.walk_range(..).unwrap();
@ -1019,7 +1032,7 @@ mod tests {
)]));
init_state.merge_transitions(BundleRetention::Reverts);
BundleStateWithReceipts::new(init_state.take_bundle(), Receipts::new(), 0)
.write_to_db(provider.tx_ref(), OriginalValuesKnown::Yes)
.write_to_storage(provider.tx_ref(), None, OriginalValuesKnown::Yes)
.expect("Could not write init bundle state to DB");
let mut state = State::builder().with_bundle_update().build();
@ -1064,12 +1077,12 @@ mod tests {
// Commit block #1 changes to the database.
state.merge_transitions(BundleRetention::Reverts);
BundleStateWithReceipts::new(state.take_bundle(), Receipts::new(), 1)
.write_to_db(provider.tx_ref(), OriginalValuesKnown::Yes)
.write_to_storage(provider.tx_ref(), None, OriginalValuesKnown::Yes)
.expect("Could not write bundle state to DB");
let mut storage_changeset_cursor = provider
.tx_ref()
.cursor_dup_read::<tables::StorageChangeSet>()
.cursor_dup_read::<tables::StorageChangeSets>()
.expect("Could not open plain storage state cursor");
let range = BlockNumberAddress::range(1..=1);
let mut storage_changes = storage_changeset_cursor.walk_range(range).unwrap();
@ -1138,9 +1151,9 @@ mod tests {
db.update(|tx| {
for (address, (account, storage)) in prestate.iter() {
let hashed_address = keccak256(address);
tx.put::<tables::HashedAccount>(hashed_address, *account).unwrap();
tx.put::<tables::HashedAccounts>(hashed_address, *account).unwrap();
for (slot, value) in storage {
tx.put::<tables::HashedStorage>(
tx.put::<tables::HashedStorages>(
hashed_address,
StorageEntry { key: keccak256(slot), value: *value },
)

View File

@ -17,7 +17,7 @@ impl HashedStateChanges {
pub fn write_to_db<TX: DbTxMut + DbTx>(self, tx: &TX) -> Result<(), DatabaseError> {
// Write hashed account updates.
let sorted_accounts = self.0.accounts.into_iter().sorted_unstable_by_key(|(key, _)| *key);
let mut hashed_accounts_cursor = tx.cursor_write::<tables::HashedAccount>()?;
let mut hashed_accounts_cursor = tx.cursor_write::<tables::HashedAccounts>()?;
for (hashed_address, account) in sorted_accounts {
if let Some(account) = account {
hashed_accounts_cursor.upsert(hashed_address, account)?;
@ -28,7 +28,7 @@ impl HashedStateChanges {
// Write hashed storage changes.
let sorted_storages = self.0.storages.into_iter().sorted_by_key(|(key, _)| *key);
let mut hashed_storage_cursor = tx.cursor_dup_write::<tables::HashedStorage>()?;
let mut hashed_storage_cursor = tx.cursor_dup_write::<tables::HashedStorages>()?;
for (hashed_address, storage) in sorted_storages {
if storage.wiped && hashed_storage_cursor.seek_exact(hashed_address)?.is_some() {
hashed_storage_cursor.delete_current_duplicates()?;
@ -74,9 +74,9 @@ mod tests {
{
let provider_rw = provider_factory.provider_rw().unwrap();
let mut accounts_cursor =
provider_rw.tx_ref().cursor_write::<tables::HashedAccount>().unwrap();
provider_rw.tx_ref().cursor_write::<tables::HashedAccounts>().unwrap();
let mut storage_cursor =
provider_rw.tx_ref().cursor_write::<tables::HashedStorage>().unwrap();
provider_rw.tx_ref().cursor_write::<tables::HashedStorages>().unwrap();
for address in addresses {
let hashed_address = keccak256(address);
@ -100,13 +100,13 @@ mod tests {
let provider = provider_factory.provider().unwrap();
assert_eq!(
provider.tx_ref().get::<tables::HashedAccount>(destroyed_address_hashed),
provider.tx_ref().get::<tables::HashedAccounts>(destroyed_address_hashed),
Ok(None)
);
assert_eq!(
provider
.tx_ref()
.cursor_read::<tables::HashedStorage>()
.cursor_read::<tables::HashedStorages>()
.unwrap()
.seek_by_key_subkey(destroyed_address_hashed, hashed_slot),
Ok(None)

View File

@ -32,7 +32,7 @@ impl StateReverts {
// Write storage changes
tracing::trace!(target: "provider::reverts", "Writing storage changes");
let mut storages_cursor = tx.cursor_dup_write::<tables::PlainStorageState>()?;
let mut storage_changeset_cursor = tx.cursor_dup_write::<tables::StorageChangeSet>()?;
let mut storage_changeset_cursor = tx.cursor_dup_write::<tables::StorageChangeSets>()?;
for (block_index, mut storage_changes) in self.0.storage.into_iter().enumerate() {
let block_number = first_block + block_index as BlockNumber;
@ -73,7 +73,7 @@ impl StateReverts {
// Write account changes
tracing::trace!(target: "provider::reverts", "Writing account changes");
let mut account_changeset_cursor = tx.cursor_dup_write::<tables::AccountChangeSet>()?;
let mut account_changeset_cursor = tx.cursor_dup_write::<tables::AccountChangeSets>()?;
for (block_index, mut account_block_reverts) in self.0.accounts.into_iter().enumerate() {
let block_number = first_block + block_index as BlockNumber;
// Sort accounts by address.

View File

@ -50,14 +50,14 @@ pub(crate) enum Action {
InsertCanonicalHeaders,
InsertHeaders,
InsertHeaderNumbers,
InsertHeaderTD,
InsertHeaderTerminalDifficulties,
InsertBlockOmmers,
InsertTxSenders,
InsertTransactionSenders,
InsertTransactions,
InsertTxHashNumbers,
InsertTransactionHashNumbers,
InsertBlockWithdrawals,
InsertBlockBodyIndices,
InsertTransactionBlock,
InsertTransactionBlocks,
GetNextTxNum,
GetParentTD,
@ -77,14 +77,14 @@ impl Action {
Action::InsertCanonicalHeaders => "insert canonical headers",
Action::InsertHeaders => "insert headers",
Action::InsertHeaderNumbers => "insert header numbers",
Action::InsertHeaderTD => "insert header TD",
Action::InsertHeaderTerminalDifficulties => "insert header TD",
Action::InsertBlockOmmers => "insert block ommers",
Action::InsertTxSenders => "insert tx senders",
Action::InsertTransactionSenders => "insert tx senders",
Action::InsertTransactions => "insert transactions",
Action::InsertTxHashNumbers => "insert tx hash numbers",
Action::InsertTransactionHashNumbers => "insert transaction hash numbers",
Action::InsertBlockWithdrawals => "insert block withdrawals",
Action::InsertBlockBodyIndices => "insert block body indices",
Action::InsertTransactionBlock => "insert transaction block",
Action::InsertTransactionBlocks => "insert transaction blocks",
Action::GetNextTxNum => "get next tx num",
Action::GetParentTD => "get parent TD",
}

View File

@ -1,8 +1,9 @@
use crate::{
providers::{
state::{historical::HistoricalStateProvider, latest::LatestStateProvider},
SnapshotProvider,
StaticFileProvider,
},
to_range,
traits::{BlockSource, ReceiptProvider},
BlockHashReader, BlockNumReader, BlockReader, ChainSpecProvider, EvmEnvProvider,
HeaderProvider, HeaderSyncGap, HeaderSyncGapProvider, HeaderSyncMode, ProviderError,
@ -13,12 +14,11 @@ use reth_db::{database::Database, init_db, models::StoredBlockBodyIndices, Datab
use reth_interfaces::{provider::ProviderResult, RethError, RethResult};
use reth_node_api::ConfigureEvmEnv;
use reth_primitives::{
snapshot::HighestSnapshots,
stage::{StageCheckpoint, StageId},
Address, Block, BlockHash, BlockHashOrNumber, BlockNumber, BlockWithSenders, ChainInfo,
ChainSpec, Header, PruneCheckpoint, PruneSegment, Receipt, SealedBlock, SealedBlockWithSenders,
SealedHeader, TransactionMeta, TransactionSigned, TransactionSignedNoHash, TxHash, TxNumber,
Withdrawal, Withdrawals, B256, U256,
SealedHeader, StaticFileSegment, TransactionMeta, TransactionSigned, TransactionSignedNoHash,
TxHash, TxNumber, Withdrawal, Withdrawals, B256, U256,
};
use revm::primitives::{BlockEnv, CfgEnvWithHandlerCfg};
use std::{
@ -26,7 +26,6 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
use tokio::sync::watch;
use tracing::trace;
mod metrics;
@ -38,49 +37,51 @@ use reth_db::mdbx::DatabaseArguments;
/// A common provider that fetches data from a database.
///
/// This provider implements most provider or provider factory traits.
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct ProviderFactory<DB> {
/// Database
db: DB,
/// Chain spec
chain_spec: Arc<ChainSpec>,
/// Snapshot Provider
snapshot_provider: Option<Arc<SnapshotProvider>>,
}
impl<DB: Clone> Clone for ProviderFactory<DB> {
fn clone(&self) -> Self {
Self {
db: self.db.clone(),
chain_spec: Arc::clone(&self.chain_spec),
snapshot_provider: self.snapshot_provider.clone(),
}
}
/// Static File Provider
static_file_provider: StaticFileProvider,
}
impl<DB> ProviderFactory<DB> {
/// Create new database provider factory.
pub fn new(db: DB, chain_spec: Arc<ChainSpec>) -> Self {
Self { db, chain_spec, snapshot_provider: None }
pub fn new(
db: DB,
chain_spec: Arc<ChainSpec>,
static_files_path: PathBuf,
) -> RethResult<ProviderFactory<DB>> {
Ok(Self {
db,
chain_spec,
static_file_provider: StaticFileProvider::new(static_files_path)?,
})
}
/// Database provider that comes with a shared snapshot provider.
pub fn with_snapshots(
mut self,
snapshots_path: PathBuf,
highest_snapshot_tracker: watch::Receiver<Option<HighestSnapshots>>,
) -> ProviderResult<Self> {
self.snapshot_provider = Some(Arc::new(
SnapshotProvider::new(snapshots_path)?
.with_highest_tracker(Some(highest_snapshot_tracker)),
));
Ok(self)
/// Enables metrics on the static file provider.
pub fn with_static_files_metrics(mut self) -> Self {
self.static_file_provider = self.static_file_provider.with_metrics();
self
}
/// Returns reference to the underlying database.
pub fn db_ref(&self) -> &DB {
&self.db
}
/// Returns static file provider
pub fn static_file_provider(&self) -> StaticFileProvider {
self.static_file_provider.clone()
}
#[cfg(any(test, feature = "test-utils"))]
/// Consumes Self and returns DB
pub fn into_db(self) -> DB {
self.db
}
}
impl ProviderFactory<DatabaseEnv> {
@ -90,11 +91,12 @@ impl ProviderFactory<DatabaseEnv> {
path: P,
chain_spec: Arc<ChainSpec>,
args: DatabaseArguments,
static_files_path: PathBuf,
) -> RethResult<Self> {
Ok(ProviderFactory::<DatabaseEnv> {
db: init_db(path, args).map_err(|e| RethError::Custom(e.to_string()))?,
chain_spec,
snapshot_provider: None,
static_file_provider: StaticFileProvider::new(static_files_path)?,
})
}
}
@ -105,13 +107,11 @@ impl<DB: Database> ProviderFactory<DB> {
/// [`BlockHashReader`]. This may fail if the inner read database transaction fails to open.
#[track_caller]
pub fn provider(&self) -> ProviderResult<DatabaseProviderRO<DB>> {
let mut provider = DatabaseProvider::new(self.db.tx()?, self.chain_spec.clone());
if let Some(snapshot_provider) = &self.snapshot_provider {
provider = provider.with_snapshot_provider(snapshot_provider.clone());
}
Ok(provider)
Ok(DatabaseProvider::new(
self.db.tx()?,
self.chain_spec.clone(),
self.static_file_provider.clone(),
))
}
/// Returns a provider with a created `DbTxMut` inside, which allows fetching and updating
@ -120,20 +120,18 @@ impl<DB: Database> ProviderFactory<DB> {
/// open.
#[track_caller]
pub fn provider_rw(&self) -> ProviderResult<DatabaseProviderRW<DB>> {
let mut provider = DatabaseProvider::new_rw(self.db.tx_mut()?, self.chain_spec.clone());
if let Some(snapshot_provider) = &self.snapshot_provider {
provider = provider.with_snapshot_provider(snapshot_provider.clone());
}
Ok(DatabaseProviderRW(provider))
Ok(DatabaseProviderRW(DatabaseProvider::new_rw(
self.db.tx_mut()?,
self.chain_spec.clone(),
self.static_file_provider.clone(),
)))
}
/// Storage provider for latest block
#[track_caller]
pub fn latest(&self) -> ProviderResult<StateProviderBox> {
trace!(target: "providers::db", "Returning latest state provider");
Ok(Box::new(LatestStateProvider::new(self.db.tx()?)))
Ok(Box::new(LatestStateProvider::new(self.db.tx()?, self.static_file_provider())))
}
/// Storage provider for state at that given block
@ -145,7 +143,10 @@ impl<DB: Database> ProviderFactory<DB> {
if block_number == provider.best_block_number().unwrap_or_default() &&
block_number == provider.last_block_number().unwrap_or_default()
{
return Ok(Box::new(LatestStateProvider::new(provider.into_tx())))
return Ok(Box::new(LatestStateProvider::new(
provider.into_tx(),
self.static_file_provider(),
)))
}
// +1 as the changeset that we want is the one that was applied after this block.
@ -156,7 +157,11 @@ impl<DB: Database> ProviderFactory<DB> {
let storage_history_prune_checkpoint =
provider.get_prune_checkpoint(PruneSegment::StorageHistory)?;
let mut state_provider = HistoricalStateProvider::new(provider.into_tx(), block_number);
let mut state_provider = HistoricalStateProvider::new(
provider.into_tx(),
block_number,
self.static_file_provider(),
);
// If we pruned account or storage history, we can't return state on every historical block.
// Instead, we should cap it at the latest prune checkpoint for corresponding prune segment.
@ -219,7 +224,12 @@ impl<DB: Database> HeaderProvider for ProviderFactory<DB> {
}
fn header_by_number(&self, num: BlockNumber) -> ProviderResult<Option<Header>> {
self.provider()?.header_by_number(num)
self.static_file_provider.get_with_static_file_or_database(
StaticFileSegment::Headers,
num,
|static_file| static_file.header_by_number(num),
|| self.provider()?.header_by_number(num),
)
}
fn header_td(&self, hash: &BlockHash) -> ProviderResult<Option<U256>> {
@ -227,22 +237,44 @@ impl<DB: Database> HeaderProvider for ProviderFactory<DB> {
}
fn header_td_by_number(&self, number: BlockNumber) -> ProviderResult<Option<U256>> {
self.provider()?.header_td_by_number(number)
if let Some(td) = self.chain_spec.final_paris_total_difficulty(number) {
// if this block is higher than the final paris(merge) block, return the final paris
// difficulty
return Ok(Some(td))
}
self.static_file_provider.get_with_static_file_or_database(
StaticFileSegment::Headers,
number,
|static_file| static_file.header_td_by_number(number),
|| self.provider()?.header_td_by_number(number),
)
}
fn headers_range(&self, range: impl RangeBounds<BlockNumber>) -> ProviderResult<Vec<Header>> {
self.provider()?.headers_range(range)
self.static_file_provider.get_range_with_static_file_or_database(
StaticFileSegment::Headers,
to_range(range),
|static_file, range, _| static_file.headers_range(range),
|range, _| self.provider()?.headers_range(range),
|_| true,
)
}
fn sealed_header(&self, number: BlockNumber) -> ProviderResult<Option<SealedHeader>> {
self.provider()?.sealed_header(number)
self.static_file_provider.get_with_static_file_or_database(
StaticFileSegment::Headers,
number,
|static_file| static_file.sealed_header(number),
|| self.provider()?.sealed_header(number),
)
}
fn sealed_headers_range(
&self,
range: impl RangeBounds<BlockNumber>,
) -> ProviderResult<Vec<SealedHeader>> {
self.provider()?.sealed_headers_range(range)
self.sealed_headers_while(range, |_| true)
}
fn sealed_headers_while(
@ -250,13 +282,24 @@ impl<DB: Database> HeaderProvider for ProviderFactory<DB> {
range: impl RangeBounds<BlockNumber>,
predicate: impl FnMut(&SealedHeader) -> bool,
) -> ProviderResult<Vec<SealedHeader>> {
self.provider()?.sealed_headers_while(range, predicate)
self.static_file_provider.get_range_with_static_file_or_database(
StaticFileSegment::Headers,
to_range(range),
|static_file, range, predicate| static_file.sealed_headers_while(range, predicate),
|range, predicate| self.provider()?.sealed_headers_while(range, predicate),
predicate,
)
}
}
impl<DB: Database> BlockHashReader for ProviderFactory<DB> {
fn block_hash(&self, number: u64) -> ProviderResult<Option<B256>> {
self.provider()?.block_hash(number)
self.static_file_provider.get_with_static_file_or_database(
StaticFileSegment::Headers,
number,
|static_file| static_file.block_hash(number),
|| self.provider()?.block_hash(number),
)
}
fn canonical_hashes_range(
@ -264,7 +307,13 @@ impl<DB: Database> BlockHashReader for ProviderFactory<DB> {
start: BlockNumber,
end: BlockNumber,
) -> ProviderResult<Vec<B256>> {
self.provider()?.canonical_hashes_range(start, end)
self.static_file_provider.get_range_with_static_file_or_database(
StaticFileSegment::Headers,
start..end,
|static_file, range, _| static_file.canonical_hashes_range(range.start, range.end),
|range, _| self.provider()?.canonical_hashes_range(range.start, range.end),
|_| true,
)
}
}
@ -337,14 +386,24 @@ impl<DB: Database> TransactionsProvider for ProviderFactory<DB> {
}
fn transaction_by_id(&self, id: TxNumber) -> ProviderResult<Option<TransactionSigned>> {
self.provider()?.transaction_by_id(id)
self.static_file_provider.get_with_static_file_or_database(
StaticFileSegment::Transactions,
id,
|static_file| static_file.transaction_by_id(id),
|| self.provider()?.transaction_by_id(id),
)
}
fn transaction_by_id_no_hash(
&self,
id: TxNumber,
) -> ProviderResult<Option<TransactionSignedNoHash>> {
self.provider()?.transaction_by_id_no_hash(id)
self.static_file_provider.get_with_static_file_or_database(
StaticFileSegment::Transactions,
id,
|static_file| static_file.transaction_by_id_no_hash(id),
|| self.provider()?.transaction_by_id_no_hash(id),
)
}
fn transaction_by_hash(&self, hash: TxHash) -> ProviderResult<Option<TransactionSigned>> {
@ -397,7 +456,12 @@ impl<DB: Database> TransactionsProvider for ProviderFactory<DB> {
impl<DB: Database> ReceiptProvider for ProviderFactory<DB> {
fn receipt(&self, id: TxNumber) -> ProviderResult<Option<Receipt>> {
self.provider()?.receipt(id)
self.static_file_provider.get_with_static_file_or_database(
StaticFileSegment::Receipts,
id,
|static_file| static_file.receipt(id),
|| self.provider()?.receipt(id),
)
}
fn receipt_by_hash(&self, hash: TxHash) -> ProviderResult<Option<Receipt>> {
@ -412,7 +476,13 @@ impl<DB: Database> ReceiptProvider for ProviderFactory<DB> {
&self,
range: impl RangeBounds<TxNumber>,
) -> ProviderResult<Vec<Receipt>> {
self.provider()?.receipts_by_tx_range(range)
self.static_file_provider.get_range_with_static_file_or_database(
StaticFileSegment::Receipts,
to_range(range),
|static_file, range, _| static_file.receipts_by_tx_range(range),
|range, _| self.provider()?.receipts_by_tx_range(range),
|_| true,
)
}
}
@ -530,13 +600,16 @@ impl<DB: Database> PruneCheckpointReader for ProviderFactory<DB> {
mod tests {
use super::ProviderFactory;
use crate::{
test_utils::create_test_provider_factory, BlockHashReader, BlockNumReader, BlockWriter,
HeaderSyncGapProvider, HeaderSyncMode, TransactionsProvider,
providers::StaticFileWriter, test_utils::create_test_provider_factory, BlockHashReader,
BlockNumReader, BlockWriter, HeaderSyncGapProvider, HeaderSyncMode, TransactionsProvider,
};
use alloy_rlp::Decodable;
use assert_matches::assert_matches;
use rand::Rng;
use reth_db::{tables, test_utils::ERROR_TEMPDIR, transaction::DbTxMut};
use reth_db::{
tables,
test_utils::{create_test_static_files_dir, ERROR_TEMPDIR},
};
use reth_interfaces::{
provider::ProviderError,
test_utils::{
@ -546,7 +619,8 @@ mod tests {
RethError,
};
use reth_primitives::{
hex_literal::hex, ChainSpecBuilder, PruneMode, PruneModes, SealedBlock, TxNumber, B256,
hex_literal::hex, ChainSpecBuilder, PruneMode, PruneModes, SealedBlock, StaticFileSegment,
TxNumber, B256, U256,
};
use std::{ops::RangeInclusive, sync::Arc};
use tokio::sync::watch;
@ -584,6 +658,7 @@ mod tests {
tempfile::TempDir::new().expect(ERROR_TEMPDIR).into_path(),
Arc::new(chain_spec),
Default::default(),
create_test_static_files_dir(),
)
.unwrap();
@ -648,7 +723,7 @@ mod tests {
Ok(_)
);
let senders = provider.get_or_take::<tables::TxSenders, true>(range.clone());
let senders = provider.get_or_take::<tables::TransactionSenders, true>(range.clone());
assert_eq!(
senders,
Ok(range
@ -687,8 +762,6 @@ mod tests {
// Genesis
let checkpoint = 0;
let head = random_header(&mut rng, 0, None);
let gap_fill = random_header(&mut rng, 1, Some(head.hash()));
let gap_tip = random_header(&mut rng, 2, Some(gap_fill.hash()));
// Empty database
assert_matches!(
@ -698,46 +771,14 @@ mod tests {
);
// Checkpoint and no gap
provider
.tx_ref()
.put::<tables::CanonicalHeaders>(head.number, head.hash())
.expect("failed to write canonical");
provider
.tx_ref()
.put::<tables::Headers>(head.number, head.clone().unseal())
.expect("failed to write header");
let mut static_file_writer =
provider.static_file_provider().latest_writer(StaticFileSegment::Headers).unwrap();
static_file_writer.append_header(head.header().clone(), U256::ZERO, head.hash()).unwrap();
static_file_writer.commit().unwrap();
drop(static_file_writer);
let gap = provider.sync_gap(mode.clone(), checkpoint).unwrap();
assert_eq!(gap.local_head, head);
assert_eq!(gap.target.tip(), consensus_tip.into());
// Checkpoint and gap
provider
.tx_ref()
.put::<tables::CanonicalHeaders>(gap_tip.number, gap_tip.hash())
.expect("failed to write canonical");
provider
.tx_ref()
.put::<tables::Headers>(gap_tip.number, gap_tip.clone().unseal())
.expect("failed to write header");
let gap = provider.sync_gap(mode.clone(), checkpoint).unwrap();
assert_eq!(gap.local_head, head);
assert_eq!(gap.target.tip(), gap_tip.parent_hash.into());
// Checkpoint and gap closed
provider
.tx_ref()
.put::<tables::CanonicalHeaders>(gap_fill.number, gap_fill.hash())
.expect("failed to write canonical");
provider
.tx_ref()
.put::<tables::Headers>(gap_fill.number, gap_fill.clone().unseal())
.expect("failed to write header");
assert_matches!(
provider.sync_gap(mode, checkpoint),
Err(RethError::Provider(ProviderError::InconsistentHeaderGap))
);
}
}

View File

@ -1,6 +1,6 @@
use crate::{
bundle_state::{BundleStateInit, BundleStateWithReceipts, HashedStateChanges, RevertsInit},
providers::{database::metrics, SnapshotProvider},
providers::{database::metrics, static_file::StaticFileWriter, StaticFileProvider},
to_range,
traits::{
AccountExtReader, BlockSource, ChangeSetReader, ReceiptProvider, StageCheckpointWriter,
@ -8,7 +8,7 @@ use crate::{
AccountReader, BlockExecutionWriter, BlockHashReader, BlockNumReader, BlockReader, BlockWriter,
Chain, EvmEnvProvider, HashingWriter, HeaderProvider, HeaderSyncGap, HeaderSyncGapProvider,
HeaderSyncMode, HistoryWriter, OriginalValuesKnown, ProviderError, PruneCheckpointReader,
PruneCheckpointWriter, StageCheckpointReader, StorageReader, TransactionVariant,
PruneCheckpointWriter, StageCheckpointReader, StatsReader, StorageReader, TransactionVariant,
TransactionsProvider, TransactionsProviderExt, WithdrawalsProvider,
};
use itertools::{izip, Itertools};
@ -28,7 +28,7 @@ use reth_db::{
use reth_interfaces::{
p2p::headers::downloader::SyncTarget,
provider::{ProviderResult, RootMismatch},
RethError, RethResult,
RethResult,
};
use reth_node_api::ConfigureEvmEnv;
use reth_primitives::{
@ -38,7 +38,7 @@ use reth_primitives::{
trie::Nibbles,
Account, Address, Block, BlockHash, BlockHashOrNumber, BlockNumber, BlockWithSenders,
ChainInfo, ChainSpec, GotExpected, Hardfork, Head, Header, PruneCheckpoint, PruneModes,
PruneSegment, Receipt, SealedBlock, SealedBlockWithSenders, SealedHeader, SnapshotSegment,
PruneSegment, Receipt, SealedBlock, SealedBlockWithSenders, SealedHeader, StaticFileSegment,
StorageEntry, TransactionMeta, TransactionSigned, TransactionSignedEcRecovered,
TransactionSignedNoHash, TxHash, TxNumber, Withdrawal, Withdrawals, B256, U256,
};
@ -49,6 +49,7 @@ use reth_trie::{
};
use revm::primitives::{BlockEnv, CfgEnvWithHandlerCfg, SpecId};
use std::{
cmp::Ordering,
collections::{hash_map, BTreeMap, BTreeSet, HashMap, HashSet},
fmt::Debug,
ops::{Bound, Deref, DerefMut, Range, RangeBounds, RangeInclusive},
@ -82,7 +83,7 @@ impl<DB: Database> DerefMut for DatabaseProviderRW<DB> {
}
impl<DB: Database> DatabaseProviderRW<DB> {
/// Commit database transaction
/// Commit database transaction and static file if it exists.
pub fn commit(self) -> ProviderResult<bool> {
self.0.commit()
}
@ -101,15 +102,25 @@ pub struct DatabaseProvider<TX> {
tx: TX,
/// Chain spec
chain_spec: Arc<ChainSpec>,
/// Snapshot provider
#[allow(dead_code)]
snapshot_provider: Option<Arc<SnapshotProvider>>,
/// Static File provider
static_file_provider: StaticFileProvider,
}
impl<TX> DatabaseProvider<TX> {
/// Returns a static file provider
pub fn static_file_provider(&self) -> &StaticFileProvider {
&self.static_file_provider
}
}
impl<TX: DbTxMut> DatabaseProvider<TX> {
/// Creates a provider with an inner read-write transaction.
pub fn new_rw(tx: TX, chain_spec: Arc<ChainSpec>) -> Self {
Self { tx, chain_spec, snapshot_provider: None }
pub fn new_rw(
tx: TX,
chain_spec: Arc<ChainSpec>,
static_file_provider: StaticFileProvider,
) -> Self {
Self { tx, chain_spec, static_file_provider }
}
}
@ -153,6 +164,29 @@ impl<TX: DbTx> DatabaseProvider<TX> {
}
}
impl<TX: DbTxMut + DbTx> DatabaseProvider<TX> {
#[cfg(any(test, feature = "test-utils"))]
/// Inserts an historical block. Used for setting up test environments
pub fn insert_historical_block(
&self,
block: SealedBlockWithSenders,
prune_modes: Option<&PruneModes>,
) -> ProviderResult<StoredBlockBodyIndices> {
let ttd = if block.number == 0 {
block.difficulty
} else {
let parent_block_number = block.number - 1;
let parent_ttd = self.header_td_by_number(parent_block_number)?.unwrap_or_default();
parent_ttd + block.difficulty
};
let mut writer = self.static_file_provider.latest_writer(StaticFileSegment::Headers)?;
writer.append_header(block.header.as_ref().clone(), ttd, block.hash())?;
self.insert_block(block, prune_modes)
}
}
/// For a given key, unwind all history shards that are below the given block number.
///
/// S - Sharded key subtype.
@ -170,7 +204,7 @@ fn unwind_history_shards<S, T, C>(
start_key: T::Key,
block_number: BlockNumber,
mut shard_belongs_to_key: impl FnMut(&T::Key) -> bool,
) -> ProviderResult<Vec<usize>>
) -> ProviderResult<Vec<u64>>
where
T: Table<Value = BlockNumberList>,
T::Key: AsRef<ShardedKey<S>>,
@ -186,15 +220,15 @@ where
// Check the first item.
// If it is greater or eq to the block number, delete it.
let first = list.iter(0).next().expect("List can't be empty");
if first >= block_number as usize {
let first = list.iter().next().expect("List can't be empty");
if first >= block_number {
item = cursor.prev()?;
continue
} else if block_number <= sharded_key.as_ref().highest_block_number {
// Filter out all elements greater than block number.
return Ok(list.iter(0).take_while(|i| *i < block_number as usize).collect::<Vec<_>>())
return Ok(list.iter().take_while(|i| *i < block_number).collect::<Vec<_>>())
} else {
return Ok(list.iter(0).collect::<Vec<_>>())
return Ok(list.iter().collect::<Vec<_>>())
}
}
@ -203,14 +237,12 @@ where
impl<TX: DbTx> DatabaseProvider<TX> {
/// Creates a provider with an inner read-only transaction.
pub fn new(tx: TX, chain_spec: Arc<ChainSpec>) -> Self {
Self { tx, chain_spec, snapshot_provider: None }
}
/// Creates a new [`Self`] with access to a [`SnapshotProvider`].
pub fn with_snapshot_provider(mut self, snapshot_provider: Arc<SnapshotProvider>) -> Self {
self.snapshot_provider = Some(snapshot_provider);
self
pub fn new(
tx: TX,
chain_spec: Arc<ChainSpec>,
static_file_provider: StaticFileProvider,
) -> Self {
Self { tx, chain_spec, static_file_provider }
}
/// Consume `DbTx` or `DbTxMut`.
@ -250,96 +282,6 @@ impl<TX: DbTx> DatabaseProvider<TX> {
self
}
/// Gets data within a specified range, potentially spanning different snapshots and database.
///
/// # Arguments
/// * `segment` - The segment of the snapshot to query.
/// * `block_range` - The range of data to fetch.
/// * `fetch_from_snapshot` - A function to fetch data from the snapshot.
/// * `fetch_from_database` - A function to fetch data from the database.
/// * `predicate` - A function used to evaluate each item in the fetched data. Fetching is
/// terminated when this function returns false, thereby filtering the data based on the
/// provided condition.
fn get_range_with_snapshot<T, P, FS, FD>(
&self,
segment: SnapshotSegment,
mut block_or_tx_range: Range<u64>,
fetch_from_snapshot: FS,
mut fetch_from_database: FD,
mut predicate: P,
) -> ProviderResult<Vec<T>>
where
FS: Fn(&SnapshotProvider, Range<u64>, &mut P) -> ProviderResult<Vec<T>>,
FD: FnMut(Range<u64>, P) -> ProviderResult<Vec<T>>,
P: FnMut(&T) -> bool,
{
let mut data = Vec::new();
if let Some(snapshot_provider) = &self.snapshot_provider {
// If there is, check the maximum block or transaction number of the segment.
if let Some(snapshot_upper_bound) = match segment {
SnapshotSegment::Headers => snapshot_provider.get_highest_snapshot_block(segment),
SnapshotSegment::Transactions | SnapshotSegment::Receipts => {
snapshot_provider.get_highest_snapshot_tx(segment)
}
} {
if block_or_tx_range.start <= snapshot_upper_bound {
let end = block_or_tx_range.end.min(snapshot_upper_bound + 1);
data.extend(fetch_from_snapshot(
snapshot_provider,
block_or_tx_range.start..end,
&mut predicate,
)?);
block_or_tx_range.start = end;
}
}
}
if block_or_tx_range.end > block_or_tx_range.start {
data.extend(fetch_from_database(block_or_tx_range, predicate)?)
}
Ok(data)
}
/// Retrieves data from the database or snapshot, wherever it's available.
///
/// # Arguments
/// * `segment` - The segment of the snapshot to check against.
/// * `index_key` - Requested index key, usually a block or transaction number.
/// * `fetch_from_snapshot` - A closure that defines how to fetch the data from the snapshot
/// provider.
/// * `fetch_from_database` - A closure that defines how to fetch the data from the database
/// when the snapshot doesn't contain the required data or is not available.
fn get_with_snapshot<T, FS, FD>(
&self,
segment: SnapshotSegment,
number: u64,
fetch_from_snapshot: FS,
fetch_from_database: FD,
) -> ProviderResult<Option<T>>
where
FS: Fn(&SnapshotProvider) -> ProviderResult<Option<T>>,
FD: Fn() -> ProviderResult<Option<T>>,
{
if let Some(provider) = &self.snapshot_provider {
// If there is, check the maximum block or transaction number of the segment.
let snapshot_upper_bound = match segment {
SnapshotSegment::Headers => provider.get_highest_snapshot_block(segment),
SnapshotSegment::Transactions | SnapshotSegment::Receipts => {
provider.get_highest_snapshot_tx(segment)
}
};
if snapshot_upper_bound
.map_or(false, |snapshot_upper_bound| snapshot_upper_bound >= number)
{
return fetch_from_snapshot(provider)
}
}
fetch_from_database()
}
fn transactions_by_tx_range_with_cursor<C>(
&self,
range: impl RangeBounds<TxNumber>,
@ -348,10 +290,10 @@ impl<TX: DbTx> DatabaseProvider<TX> {
where
C: DbCursorRO<tables::Transactions>,
{
self.get_range_with_snapshot(
SnapshotSegment::Transactions,
self.static_file_provider.get_range_with_static_file_or_database(
StaticFileSegment::Transactions,
to_range(range),
|snapshot, range, _| snapshot.transactions_by_tx_range(range),
|static_file, range, _| static_file.transactions_by_tx_range(range),
|range, _| self.cursor_collect(cursor, range),
|_| true,
)
@ -375,9 +317,9 @@ impl<TX: DbTxMut + DbTx> DatabaseProvider<TX> {
///
/// 1. Iterate over the [BlockBodyIndices][tables::BlockBodyIndices] table to get all
/// the transaction ids.
/// 2. Iterate over the [StorageChangeSet][tables::StorageChangeSet] table
/// and the [AccountChangeSet][tables::AccountChangeSet] tables in reverse order to reconstruct
/// the changesets.
/// 2. Iterate over the [StorageChangeSets][tables::StorageChangeSets] table
/// and the [AccountChangeSets][tables::AccountChangeSets] tables in reverse order to
/// reconstruct the changesets.
/// - In order to have both the old and new values in the changesets, we also access the
/// plain state tables.
/// 3. While iterating over the changeset tables, if we encounter a new account or storage slot,
@ -411,8 +353,8 @@ impl<TX: DbTxMut + DbTx> DatabaseProvider<TX> {
let storage_range = BlockNumberAddress::range(range.clone());
let storage_changeset =
self.get_or_take::<tables::StorageChangeSet, UNWIND>(storage_range)?;
let account_changeset = self.get_or_take::<tables::AccountChangeSet, UNWIND>(range)?;
self.get_or_take::<tables::StorageChangeSets, UNWIND>(storage_range)?;
let account_changeset = self.get_or_take::<tables::AccountChangeSets, UNWIND>(range)?;
// iterate previous value and get plain state value to create changeset
// Double option around Account represent if Account state is know (first option) and
@ -592,8 +534,9 @@ impl<TX: DbTxMut + DbTx> DatabaseProvider<TX> {
.map(|(id, tx)| (id, tx.into()))
.collect::<Vec<(u64, TransactionSigned)>>();
let mut senders =
self.get_or_take::<tables::TxSenders, TAKE>(first_transaction..=last_transaction)?;
let mut senders = self.get_or_take::<tables::TransactionSenders, TAKE>(
first_transaction..=last_transaction,
)?;
// Recover senders manually if not found in db
// NOTE: Transactions are always guaranteed to be in the database whereas
@ -655,18 +598,18 @@ impl<TX: DbTxMut + DbTx> DatabaseProvider<TX> {
}
if TAKE {
// Remove TxHashNumber
let mut tx_hash_cursor = self.tx.cursor_write::<tables::TxHashNumber>()?;
// Remove TransactionHashNumbers
let mut tx_hash_cursor = self.tx.cursor_write::<tables::TransactionHashNumbers>()?;
for (_, tx) in transactions.iter() {
if tx_hash_cursor.seek_exact(tx.hash())?.is_some() {
tx_hash_cursor.delete_current()?;
}
}
// Remove TransactionBlock index if there are transaction present
// Remove TransactionBlocks index if there are transaction present
if !transactions.is_empty() {
let tx_id_range = transactions.first().unwrap().0..=transactions.last().unwrap().0;
self.get_or_take::<tables::TransactionBlock, TAKE>(tx_id_range)?;
self.get_or_take::<tables::TransactionBlocks, TAKE>(tx_id_range)?;
}
}
@ -723,8 +666,8 @@ impl<TX: DbTxMut + DbTx> DatabaseProvider<TX> {
let block_tx = self.get_take_block_transaction_range::<TAKE>(range.clone())?;
if TAKE {
// rm HeaderTD
self.get_or_take::<tables::HeaderTD, TAKE>(range)?;
// rm HeaderTerminalDifficulties
self.get_or_take::<tables::HeaderTerminalDifficulties, TAKE>(range)?;
// rm HeaderNumbers
let mut header_number_cursor = self.tx.cursor_write::<tables::HeaderNumbers>()?;
for (_, hash) in block_header_hashes.iter() {
@ -914,7 +857,7 @@ impl<TX: DbTxMut + DbTx> DatabaseProvider<TX> {
if let Some((shard_key, list)) = shard {
// delete old shard so new one can be inserted.
self.tx.delete::<T>(shard_key, None)?;
let list = list.iter(0).map(|i| i as u64).collect::<Vec<_>>();
let list = list.iter().collect::<Vec<_>>();
return Ok(list)
}
Ok(Vec::new())
@ -943,13 +886,13 @@ impl<TX: DbTxMut + DbTx> DatabaseProvider<TX> {
let chunks = indices
.chunks(sharded_key::NUM_OF_INDICES_IN_SHARD)
.into_iter()
.map(|chunks| chunks.map(|i| *i as usize).collect::<Vec<usize>>())
.collect::<Vec<_>>();
.map(|chunks| chunks.copied().collect())
.collect::<Vec<Vec<_>>>();
let mut chunks = chunks.into_iter().peekable();
while let Some(list) = chunks.next() {
let highest_block_number = if chunks.peek().is_some() {
*list.last().expect("`chunks` does not return empty list") as u64
*list.last().expect("`chunks` does not return empty list")
} else {
// Insert last list with u64::MAX
u64::MAX
@ -976,7 +919,7 @@ impl<TX: DbTx> AccountExtReader for DatabaseProvider<TX> {
range: impl RangeBounds<BlockNumber>,
) -> ProviderResult<BTreeSet<Address>> {
self.tx
.cursor_read::<tables::AccountChangeSet>()?
.cursor_read::<tables::AccountChangeSets>()?
.walk_range(range)?
.map(|entry| {
entry.map(|(_, account_before)| account_before.address).map_err(Into::into)
@ -999,7 +942,7 @@ impl<TX: DbTx> AccountExtReader for DatabaseProvider<TX> {
&self,
range: RangeInclusive<BlockNumber>,
) -> ProviderResult<BTreeMap<Address, Vec<u64>>> {
let mut changeset_cursor = self.tx.cursor_read::<tables::AccountChangeSet>()?;
let mut changeset_cursor = self.tx.cursor_read::<tables::AccountChangeSets>()?;
let account_transitions = changeset_cursor.walk_range(range)?.try_fold(
BTreeMap::new(),
@ -1021,7 +964,7 @@ impl<TX: DbTx> ChangeSetReader for DatabaseProvider<TX> {
) -> ProviderResult<Vec<AccountBeforeTx>> {
let range = block_number..=block_number;
self.tx
.cursor_read::<tables::AccountChangeSet>()?
.cursor_read::<tables::AccountChangeSets>()?
.walk_range(range)?
.map(|result| -> ProviderResult<_> {
let (_, account_before) = result?;
@ -1037,45 +980,38 @@ impl<TX: DbTx> HeaderSyncGapProvider for DatabaseProvider<TX> {
mode: HeaderSyncMode,
highest_uninterrupted_block: BlockNumber,
) -> RethResult<HeaderSyncGap> {
// Create a cursor over canonical header hashes
let mut cursor = self.tx.cursor_read::<tables::CanonicalHeaders>()?;
let mut header_cursor = self.tx.cursor_read::<tables::Headers>()?;
let static_file_provider = self.static_file_provider();
// Get head hash and reposition the cursor
let (head_num, head_hash) = cursor
.seek_exact(highest_uninterrupted_block)?
// Make sure Headers static file is at the same height. If it's further, this
// input execution was interrupted previously and we need to unwind the static file.
let next_static_file_block_num = static_file_provider
.get_highest_static_file_block(StaticFileSegment::Headers)
.map(|id| id + 1)
.unwrap_or_default();
let next_block = highest_uninterrupted_block + 1;
match next_static_file_block_num.cmp(&next_block) {
// The node shutdown between an executed static file commit and before the database
// commit, so we need to unwind the static files.
Ordering::Greater => {
let mut static_file_producer =
static_file_provider.latest_writer(StaticFileSegment::Headers)?;
static_file_producer.prune_headers(next_static_file_block_num - next_block)?
}
Ordering::Less => {
// There's either missing or corrupted files.
return Err(ProviderError::HeaderNotFound(next_static_file_block_num.into()).into())
}
Ordering::Equal => {}
}
let local_head = static_file_provider
.sealed_header(highest_uninterrupted_block)?
.ok_or_else(|| ProviderError::HeaderNotFound(highest_uninterrupted_block.into()))?;
// Construct head
let (_, head) = header_cursor
.seek_exact(head_num)?
.ok_or_else(|| ProviderError::HeaderNotFound(head_num.into()))?;
let local_head = head.seal(head_hash);
// Look up the next header
let next_header = cursor
.next()?
.map(|(next_num, next_hash)| -> Result<SealedHeader, RethError> {
let (_, next) = header_cursor
.seek_exact(next_num)?
.ok_or_else(|| ProviderError::HeaderNotFound(next_num.into()))?;
Ok(next.seal(next_hash))
})
.transpose()?;
// Decide the tip or error out on invalid input.
// If the next element found in the cursor is not the "expected" next block per our current
// checkpoint, then there is a gap in the database and we should start downloading in
// reverse from there. Else, it should use whatever the forkchoice state reports.
let target = match next_header {
Some(header) if highest_uninterrupted_block + 1 != header.number => {
SyncTarget::Gap(header)
}
None => match mode {
HeaderSyncMode::Tip(rx) => SyncTarget::Tip(*rx.borrow()),
HeaderSyncMode::Continuous => SyncTarget::TipNum(head_num + 1),
},
_ => return Err(ProviderError::InconsistentHeaderGap.into()),
let target = match mode {
HeaderSyncMode::Tip(rx) => SyncTarget::Tip(*rx.borrow()),
HeaderSyncMode::Continuous => SyncTarget::TipNum(highest_uninterrupted_block + 1),
};
Ok(HeaderSyncGap { local_head, target })
@ -1092,10 +1028,10 @@ impl<TX: DbTx> HeaderProvider for DatabaseProvider<TX> {
}
fn header_by_number(&self, num: BlockNumber) -> ProviderResult<Option<Header>> {
self.get_with_snapshot(
SnapshotSegment::Headers,
self.static_file_provider.get_with_static_file_or_database(
StaticFileSegment::Headers,
num,
|snapshot| snapshot.header_by_number(num),
|static_file| static_file.header_by_number(num),
|| Ok(self.tx.get::<tables::Headers>(num)?),
)
}
@ -1115,29 +1051,29 @@ impl<TX: DbTx> HeaderProvider for DatabaseProvider<TX> {
return Ok(Some(td))
}
self.get_with_snapshot(
SnapshotSegment::Headers,
self.static_file_provider.get_with_static_file_or_database(
StaticFileSegment::Headers,
number,
|snapshot| snapshot.header_td_by_number(number),
|| Ok(self.tx.get::<tables::HeaderTD>(number)?.map(|td| td.0)),
|static_file| static_file.header_td_by_number(number),
|| Ok(self.tx.get::<tables::HeaderTerminalDifficulties>(number)?.map(|td| td.0)),
)
}
fn headers_range(&self, range: impl RangeBounds<BlockNumber>) -> ProviderResult<Vec<Header>> {
self.get_range_with_snapshot(
SnapshotSegment::Headers,
self.static_file_provider.get_range_with_static_file_or_database(
StaticFileSegment::Headers,
to_range(range),
|snapshot, range, _| snapshot.headers_range(range),
|static_file, range, _| static_file.headers_range(range),
|range, _| self.cursor_read_collect::<tables::Headers>(range).map_err(Into::into),
|_| true,
)
}
fn sealed_header(&self, number: BlockNumber) -> ProviderResult<Option<SealedHeader>> {
self.get_with_snapshot(
SnapshotSegment::Headers,
self.static_file_provider.get_with_static_file_or_database(
StaticFileSegment::Headers,
number,
|snapshot| snapshot.sealed_header(number),
|static_file| static_file.sealed_header(number),
|| {
if let Some(header) = self.header_by_number(number)? {
let hash = self
@ -1156,10 +1092,10 @@ impl<TX: DbTx> HeaderProvider for DatabaseProvider<TX> {
range: impl RangeBounds<BlockNumber>,
predicate: impl FnMut(&SealedHeader) -> bool,
) -> ProviderResult<Vec<SealedHeader>> {
self.get_range_with_snapshot(
SnapshotSegment::Headers,
self.static_file_provider.get_range_with_static_file_or_database(
StaticFileSegment::Headers,
to_range(range),
|snapshot, range, predicate| snapshot.sealed_headers_while(range, predicate),
|static_file, range, predicate| static_file.sealed_headers_while(range, predicate),
|range, mut predicate| {
let mut headers = vec![];
for entry in self.tx.cursor_read::<tables::Headers>()?.walk_range(range)? {
@ -1182,10 +1118,10 @@ impl<TX: DbTx> HeaderProvider for DatabaseProvider<TX> {
impl<TX: DbTx> BlockHashReader for DatabaseProvider<TX> {
fn block_hash(&self, number: u64) -> ProviderResult<Option<B256>> {
self.get_with_snapshot(
SnapshotSegment::Headers,
self.static_file_provider.get_with_static_file_or_database(
StaticFileSegment::Headers,
number,
|snapshot| snapshot.block_hash(number),
|static_file| static_file.block_hash(number),
|| Ok(self.tx.get::<tables::CanonicalHeaders>(number)?),
)
}
@ -1195,10 +1131,10 @@ impl<TX: DbTx> BlockHashReader for DatabaseProvider<TX> {
start: BlockNumber,
end: BlockNumber,
) -> ProviderResult<Vec<B256>> {
self.get_range_with_snapshot(
SnapshotSegment::Headers,
self.static_file_provider.get_range_with_static_file_or_database(
StaticFileSegment::Headers,
start..end,
|snapshot, range, _| snapshot.canonical_hashes_range(range.start, range.end),
|static_file, range, _| static_file.canonical_hashes_range(range.start, range.end),
|range, _| {
self.cursor_read_collect::<tables::CanonicalHeaders>(range).map_err(Into::into)
},
@ -1418,10 +1354,10 @@ impl<TX: DbTx> TransactionsProviderExt for DatabaseProvider<TX> {
&self,
tx_range: Range<TxNumber>,
) -> ProviderResult<Vec<(TxHash, TxNumber)>> {
self.get_range_with_snapshot(
SnapshotSegment::Transactions,
self.static_file_provider.get_range_with_static_file_or_database(
StaticFileSegment::Transactions,
tx_range,
|snapshot, range, _| snapshot.transaction_hashes_by_range(range),
|static_file, range, _| static_file.transaction_hashes_by_range(range),
|tx_range, _| {
let mut tx_cursor = self.tx.cursor_read::<tables::Transactions>()?;
let tx_range_size = tx_range.clone().count();
@ -1482,14 +1418,14 @@ impl<TX: DbTx> TransactionsProviderExt for DatabaseProvider<TX> {
impl<TX: DbTx> TransactionsProvider for DatabaseProvider<TX> {
fn transaction_id(&self, tx_hash: TxHash) -> ProviderResult<Option<TxNumber>> {
Ok(self.tx.get::<tables::TxHashNumber>(tx_hash)?)
Ok(self.tx.get::<tables::TransactionHashNumbers>(tx_hash)?)
}
fn transaction_by_id(&self, id: TxNumber) -> ProviderResult<Option<TransactionSigned>> {
self.get_with_snapshot(
SnapshotSegment::Transactions,
self.static_file_provider.get_with_static_file_or_database(
StaticFileSegment::Transactions,
id,
|snapshot| snapshot.transaction_by_id(id),
|static_file| static_file.transaction_by_id(id),
|| Ok(self.tx.get::<tables::Transactions>(id)?.map(Into::into)),
)
}
@ -1498,10 +1434,10 @@ impl<TX: DbTx> TransactionsProvider for DatabaseProvider<TX> {
&self,
id: TxNumber,
) -> ProviderResult<Option<TransactionSignedNoHash>> {
self.get_with_snapshot(
SnapshotSegment::Transactions,
self.static_file_provider.get_with_static_file_or_database(
StaticFileSegment::Transactions,
id,
|snapshot| snapshot.transaction_by_id_no_hash(id),
|static_file| static_file.transaction_by_id_no_hash(id),
|| Ok(self.tx.get::<tables::Transactions>(id)?),
)
}
@ -1523,7 +1459,7 @@ impl<TX: DbTx> TransactionsProvider for DatabaseProvider<TX> {
&self,
tx_hash: TxHash,
) -> ProviderResult<Option<(TransactionSigned, TransactionMeta)>> {
let mut transaction_cursor = self.tx.cursor_read::<tables::TransactionBlock>()?;
let mut transaction_cursor = self.tx.cursor_read::<tables::TransactionBlocks>()?;
if let Some(transaction_id) = self.transaction_id(tx_hash)? {
if let Some(tx) = self.transaction_by_id_no_hash(transaction_id)? {
let transaction = TransactionSigned {
@ -1563,7 +1499,7 @@ impl<TX: DbTx> TransactionsProvider for DatabaseProvider<TX> {
}
fn transaction_block(&self, id: TxNumber) -> ProviderResult<Option<BlockNumber>> {
let mut cursor = self.tx.cursor_read::<tables::TransactionBlock>()?;
let mut cursor = self.tx.cursor_read::<tables::TransactionBlocks>()?;
Ok(cursor.seek(id)?.map(|(_, bn)| bn))
}
@ -1629,20 +1565,20 @@ impl<TX: DbTx> TransactionsProvider for DatabaseProvider<TX> {
&self,
range: impl RangeBounds<TxNumber>,
) -> ProviderResult<Vec<Address>> {
self.cursor_read_collect::<tables::TxSenders>(range).map_err(Into::into)
self.cursor_read_collect::<tables::TransactionSenders>(range).map_err(Into::into)
}
fn transaction_sender(&self, id: TxNumber) -> ProviderResult<Option<Address>> {
Ok(self.tx.get::<tables::TxSenders>(id)?)
Ok(self.tx.get::<tables::TransactionSenders>(id)?)
}
}
impl<TX: DbTx> ReceiptProvider for DatabaseProvider<TX> {
fn receipt(&self, id: TxNumber) -> ProviderResult<Option<Receipt>> {
self.get_with_snapshot(
SnapshotSegment::Receipts,
self.static_file_provider.get_with_static_file_or_database(
StaticFileSegment::Receipts,
id,
|snapshot| snapshot.receipt(id),
|static_file| static_file.receipt(id),
|| Ok(self.tx.get::<tables::Receipts>(id)?),
)
}
@ -1673,10 +1609,10 @@ impl<TX: DbTx> ReceiptProvider for DatabaseProvider<TX> {
&self,
range: impl RangeBounds<TxNumber>,
) -> ProviderResult<Vec<Receipt>> {
self.get_range_with_snapshot(
SnapshotSegment::Receipts,
self.static_file_provider.get_range_with_static_file_or_database(
StaticFileSegment::Receipts,
to_range(range),
|snapshot, range, _| snapshot.receipts_by_tx_range(range),
|static_file, range, _| static_file.receipts_by_tx_range(range),
|range, _| self.cursor_read_collect::<tables::Receipts>(range).map_err(Into::into),
|_| true,
)
@ -1818,12 +1754,12 @@ impl<TX: DbTx> EvmEnvProvider for DatabaseProvider<TX> {
impl<TX: DbTx> StageCheckpointReader for DatabaseProvider<TX> {
fn get_stage_checkpoint(&self, id: StageId) -> ProviderResult<Option<StageCheckpoint>> {
Ok(self.tx.get::<tables::SyncStage>(id.to_string())?)
Ok(self.tx.get::<tables::StageCheckpoints>(id.to_string())?)
}
/// Get stage checkpoint progress.
fn get_stage_checkpoint_progress(&self, id: StageId) -> ProviderResult<Option<Vec<u8>>> {
Ok(self.tx.get::<tables::SyncStageProgress>(id.to_string())?)
Ok(self.tx.get::<tables::StageCheckpointProgresses>(id.to_string())?)
}
}
@ -1834,7 +1770,7 @@ impl<TX: DbTxMut> StageCheckpointWriter for DatabaseProvider<TX> {
id: StageId,
checkpoint: StageCheckpoint,
) -> ProviderResult<()> {
Ok(self.tx.put::<tables::SyncStage>(id.to_string(), checkpoint)?)
Ok(self.tx.put::<tables::StageCheckpoints>(id.to_string(), checkpoint)?)
}
/// Save stage checkpoint progress.
@ -1843,7 +1779,7 @@ impl<TX: DbTxMut> StageCheckpointWriter for DatabaseProvider<TX> {
id: StageId,
checkpoint: Vec<u8>,
) -> ProviderResult<()> {
Ok(self.tx.put::<tables::SyncStageProgress>(id.to_string(), checkpoint)?)
Ok(self.tx.put::<tables::StageCheckpointProgresses>(id.to_string(), checkpoint)?)
}
fn update_pipeline_stages(
@ -1852,7 +1788,7 @@ impl<TX: DbTxMut> StageCheckpointWriter for DatabaseProvider<TX> {
drop_stage_checkpoint: bool,
) -> ProviderResult<()> {
// iterate over all existing stages in the table and update its progress.
let mut cursor = self.tx.cursor_write::<tables::SyncStage>()?;
let mut cursor = self.tx.cursor_write::<tables::StageCheckpoints>()?;
for stage_id in StageId::ALL {
let (_, checkpoint) = cursor.seek_exact(stage_id.to_string())?.unwrap_or_default();
cursor.upsert(
@ -1897,7 +1833,7 @@ impl<TX: DbTx> StorageReader for DatabaseProvider<TX> {
range: RangeInclusive<BlockNumber>,
) -> ProviderResult<BTreeMap<Address, BTreeSet<B256>>> {
self.tx
.cursor_read::<tables::StorageChangeSet>()?
.cursor_read::<tables::StorageChangeSets>()?
.walk_range(BlockNumberAddress::range(range))?
// fold all storages and save its old state so we can remove it from HashedStorage
// it is needed as it is dup table.
@ -1912,7 +1848,7 @@ impl<TX: DbTx> StorageReader for DatabaseProvider<TX> {
&self,
range: RangeInclusive<BlockNumber>,
) -> ProviderResult<BTreeMap<(Address, B256), Vec<u64>>> {
let mut changeset_cursor = self.tx.cursor_read::<tables::StorageChangeSet>()?;
let mut changeset_cursor = self.tx.cursor_read::<tables::StorageChangeSets>()?;
let storage_changeset_lists =
changeset_cursor.walk_range(BlockNumberAddress::range(range))?.try_fold(
@ -1941,7 +1877,7 @@ impl<TX: DbTxMut + DbTx> HashingWriter for DatabaseProvider<TX> {
// changes are applied in the correct order.
let hashed_accounts = self
.tx
.cursor_read::<tables::AccountChangeSet>()?
.cursor_read::<tables::AccountChangeSets>()?
.walk_range(range)?
.map(|entry| entry.map(|(_, e)| (keccak256(e.address), e.info)))
.collect::<Result<Vec<_>, _>>()?
@ -1950,7 +1886,7 @@ impl<TX: DbTxMut + DbTx> HashingWriter for DatabaseProvider<TX> {
.collect::<BTreeMap<_, _>>();
// Apply values to HashedState, and remove the account if it's None.
let mut hashed_accounts_cursor = self.tx.cursor_write::<tables::HashedAccount>()?;
let mut hashed_accounts_cursor = self.tx.cursor_write::<tables::HashedAccounts>()?;
for (hashed_address, account) in &hashed_accounts {
if let Some(account) = account {
hashed_accounts_cursor.upsert(*hashed_address, *account)?;
@ -1966,7 +1902,7 @@ impl<TX: DbTxMut + DbTx> HashingWriter for DatabaseProvider<TX> {
&self,
accounts: impl IntoIterator<Item = (Address, Option<Account>)>,
) -> ProviderResult<BTreeMap<B256, Option<Account>>> {
let mut hashed_accounts_cursor = self.tx.cursor_write::<tables::HashedAccount>()?;
let mut hashed_accounts_cursor = self.tx.cursor_write::<tables::HashedAccounts>()?;
let hashed_accounts =
accounts.into_iter().map(|(ad, ac)| (keccak256(ad), ac)).collect::<BTreeMap<_, _>>();
for (hashed_address, account) in &hashed_accounts {
@ -1984,7 +1920,7 @@ impl<TX: DbTxMut + DbTx> HashingWriter for DatabaseProvider<TX> {
range: Range<BlockNumberAddress>,
) -> ProviderResult<HashMap<B256, BTreeSet<B256>>> {
// Aggregate all block changesets and make list of accounts that have been changed.
let mut changesets = self.tx.cursor_read::<tables::StorageChangeSet>()?;
let mut changesets = self.tx.cursor_read::<tables::StorageChangeSets>()?;
let mut hashed_storages = changesets
.walk_range(range)?
.map(|entry| {
@ -1997,7 +1933,7 @@ impl<TX: DbTxMut + DbTx> HashingWriter for DatabaseProvider<TX> {
// Apply values to HashedState, and remove the account if it's None.
let mut hashed_storage_keys: HashMap<B256, BTreeSet<B256>> = HashMap::new();
let mut hashed_storage = self.tx.cursor_dup_write::<tables::HashedStorage>()?;
let mut hashed_storage = self.tx.cursor_dup_write::<tables::HashedStorages>()?;
for (hashed_address, key, value) in hashed_storages.into_iter().rev() {
hashed_storage_keys.entry(hashed_address).or_default().insert(key);
@ -2036,7 +1972,7 @@ impl<TX: DbTxMut + DbTx> HashingWriter for DatabaseProvider<TX> {
(*hashed_address, BTreeSet::from_iter(entries.keys().copied()))
}));
let mut hashed_storage_cursor = self.tx.cursor_dup_write::<tables::HashedStorage>()?;
let mut hashed_storage_cursor = self.tx.cursor_dup_write::<tables::HashedStorages>()?;
// Hash the address and key and apply them to HashedStorage (if Storage is None
// just remove it);
hashed_storages.into_iter().try_for_each(|(hashed_address, storage)| {
@ -2157,7 +2093,7 @@ impl<TX: DbTxMut + DbTx> HistoryWriter for DatabaseProvider<TX> {
&self,
storage_transitions: BTreeMap<(Address, B256), Vec<u64>>,
) -> ProviderResult<()> {
self.append_history_index::<_, tables::StorageHistory>(
self.append_history_index::<_, tables::StoragesHistory>(
storage_transitions,
|(address, storage_key), highest_block_number| {
StorageShardedKey::new(address, storage_key, highest_block_number)
@ -2169,7 +2105,10 @@ impl<TX: DbTxMut + DbTx> HistoryWriter for DatabaseProvider<TX> {
&self,
account_transitions: BTreeMap<Address, Vec<u64>>,
) -> ProviderResult<()> {
self.append_history_index::<_, tables::AccountHistory>(account_transitions, ShardedKey::new)
self.append_history_index::<_, tables::AccountsHistory>(
account_transitions,
ShardedKey::new,
)
}
fn unwind_storage_history_indices(
@ -2178,7 +2117,7 @@ impl<TX: DbTxMut + DbTx> HistoryWriter for DatabaseProvider<TX> {
) -> ProviderResult<usize> {
let mut storage_changesets = self
.tx
.cursor_read::<tables::StorageChangeSet>()?
.cursor_read::<tables::StorageChangeSets>()?
.walk_range(range)?
.map(|entry| {
entry.map(|(BlockNumberAddress((bn, address)), storage)| (address, storage.key, bn))
@ -2186,9 +2125,9 @@ impl<TX: DbTxMut + DbTx> HistoryWriter for DatabaseProvider<TX> {
.collect::<Result<Vec<_>, _>>()?;
storage_changesets.sort_by_key(|(address, key, _)| (*address, *key));
let mut cursor = self.tx.cursor_write::<tables::StorageHistory>()?;
let mut cursor = self.tx.cursor_write::<tables::StoragesHistory>()?;
for &(address, storage_key, rem_index) in &storage_changesets {
let partial_shard = unwind_history_shards::<_, tables::StorageHistory, _>(
let partial_shard = unwind_history_shards::<_, tables::StoragesHistory, _>(
&mut cursor,
StorageShardedKey::last(address, storage_key),
rem_index,
@ -2218,16 +2157,16 @@ impl<TX: DbTxMut + DbTx> HistoryWriter for DatabaseProvider<TX> {
) -> ProviderResult<usize> {
let mut last_indices = self
.tx
.cursor_read::<tables::AccountChangeSet>()?
.cursor_read::<tables::AccountChangeSets>()?
.walk_range(range)?
.map(|entry| entry.map(|(index, account)| (account.address, index)))
.collect::<Result<Vec<_>, _>>()?;
last_indices.sort_by_key(|(a, _)| *a);
// Unwind the account history index.
let mut cursor = self.tx.cursor_write::<tables::AccountHistory>()?;
let mut cursor = self.tx.cursor_write::<tables::AccountsHistory>()?;
for &(address, rem_index) in &last_indices {
let partial_shard = unwind_history_shards::<_, tables::AccountHistory, _>(
let partial_shard = unwind_history_shards::<_, tables::AccountsHistory, _>(
&mut cursor,
ShardedKey::last(address),
rem_index,
@ -2375,8 +2314,8 @@ impl<TX: DbTxMut + DbTx> BlockWriter for DatabaseProvider<TX> {
parent_ttd + block.difficulty
};
self.tx.put::<tables::HeaderTD>(block_number, ttd.into())?;
durations_recorder.record_relative(metrics::Action::InsertHeaderTD);
self.tx.put::<tables::HeaderTerminalDifficulties>(block_number, ttd.into())?;
durations_recorder.record_relative(metrics::Action::InsertHeaderTerminalDifficulties);
// insert body ommers data
if !block.ommers.is_empty() {
@ -2389,7 +2328,7 @@ impl<TX: DbTxMut + DbTx> BlockWriter for DatabaseProvider<TX> {
let mut next_tx_num = self
.tx
.cursor_read::<tables::Transactions>()?
.cursor_read::<tables::TransactionBlocks>()?
.last()?
.map(|(n, _)| n + 1)
.unwrap_or_default();
@ -2412,7 +2351,7 @@ impl<TX: DbTxMut + DbTx> BlockWriter for DatabaseProvider<TX> {
.is_none()
{
let start = Instant::now();
self.tx.put::<tables::TxSenders>(next_tx_num, *sender)?;
self.tx.put::<tables::TransactionSenders>(next_tx_num, *sender)?;
tx_senders_elapsed += start.elapsed();
}
@ -2437,16 +2376,19 @@ impl<TX: DbTxMut + DbTx> BlockWriter for DatabaseProvider<TX> {
.is_none()
{
let start = Instant::now();
self.tx.put::<tables::TxHashNumber>(hash, next_tx_num)?;
self.tx.put::<tables::TransactionHashNumbers>(hash, next_tx_num)?;
tx_hash_numbers_elapsed += start.elapsed();
}
next_tx_num += 1;
}
durations_recorder.record_duration(metrics::Action::InsertTxSenders, tx_senders_elapsed);
durations_recorder
.record_duration(metrics::Action::InsertTransactionSenders, tx_senders_elapsed);
durations_recorder
.record_duration(metrics::Action::InsertTransactions, transactions_elapsed);
durations_recorder
.record_duration(metrics::Action::InsertTxHashNumbers, tx_hash_numbers_elapsed);
durations_recorder.record_duration(
metrics::Action::InsertTransactionHashNumbers,
tx_hash_numbers_elapsed,
);
if let Some(withdrawals) = block.block.withdrawals {
if !withdrawals.is_empty() {
@ -2463,8 +2405,8 @@ impl<TX: DbTxMut + DbTx> BlockWriter for DatabaseProvider<TX> {
durations_recorder.record_relative(metrics::Action::InsertBlockBodyIndices);
if !block_indices.is_empty() {
self.tx.put::<tables::TransactionBlock>(block_indices.last_tx_num(), block_number)?;
durations_recorder.record_relative(metrics::Action::InsertTransactionBlock);
self.tx.put::<tables::TransactionBlocks>(block_indices.last_tx_num(), block_number)?;
durations_recorder.record_relative(metrics::Action::InsertTransactionBlocks);
}
debug!(
@ -2505,7 +2447,7 @@ impl<TX: DbTxMut + DbTx> BlockWriter for DatabaseProvider<TX> {
// Write state and changesets to the database.
// Must be written after blocks because of the receipt lookup.
state.write_to_db(self.tx_ref(), OriginalValuesKnown::No)?;
state.write_to_storage(self.tx_ref(), None, OriginalValuesKnown::No)?;
durations_recorder.record_relative(metrics::Action::InsertState);
// insert hashes and intermediate merkle nodes
@ -2547,6 +2489,19 @@ impl<TX: DbTxMut> PruneCheckpointWriter for DatabaseProvider<TX> {
}
}
impl<TX: DbTx> StatsReader for DatabaseProvider<TX> {
fn count_entries<T: Table>(&self) -> ProviderResult<usize> {
let db_entries = self.tx.entries::<T>()?;
let static_file_entries = match self.static_file_provider.count_entries::<T>() {
Ok(entries) => entries,
Err(ProviderError::UnsupportedProvider) => 0,
Err(err) => return Err(err),
};
Ok(db_entries + static_file_entries)
}
}
fn range_size_hint(range: &impl RangeBounds<TxNumber>) -> Option<usize> {
let start = match range.start_bound().cloned() {
Bound::Included(start) => start,

View File

@ -39,8 +39,11 @@ pub use state::{
mod bundle_state_provider;
mod chain_info;
mod database;
mod snapshot;
pub use snapshot::{SnapshotJarProvider, SnapshotProvider};
mod static_file;
pub use static_file::{
StaticFileJarProvider, StaticFileProvider, StaticFileProviderRW, StaticFileProviderRWRefMut,
StaticFileWriter,
};
mod state;
use crate::{providers::chain_info::ChainInfoTracker, traits::BlockSource};
pub use bundle_state_provider::BundleStateProvider;
@ -131,34 +134,34 @@ where
Tree: Send + Sync,
{
fn header(&self, block_hash: &BlockHash) -> ProviderResult<Option<Header>> {
self.database.provider()?.header(block_hash)
self.database.header(block_hash)
}
fn header_by_number(&self, num: BlockNumber) -> ProviderResult<Option<Header>> {
self.database.provider()?.header_by_number(num)
self.database.header_by_number(num)
}
fn header_td(&self, hash: &BlockHash) -> ProviderResult<Option<U256>> {
self.database.provider()?.header_td(hash)
self.database.header_td(hash)
}
fn header_td_by_number(&self, number: BlockNumber) -> ProviderResult<Option<U256>> {
self.database.provider()?.header_td_by_number(number)
self.database.header_td_by_number(number)
}
fn headers_range(&self, range: impl RangeBounds<BlockNumber>) -> ProviderResult<Vec<Header>> {
self.database.provider()?.headers_range(range)
self.database.headers_range(range)
}
fn sealed_header(&self, number: BlockNumber) -> ProviderResult<Option<SealedHeader>> {
self.database.provider()?.sealed_header(number)
self.database.sealed_header(number)
}
fn sealed_headers_range(
&self,
range: impl RangeBounds<BlockNumber>,
) -> ProviderResult<Vec<SealedHeader>> {
self.database.provider()?.sealed_headers_range(range)
self.database.sealed_headers_range(range)
}
fn sealed_headers_while(
@ -166,7 +169,7 @@ where
range: impl RangeBounds<BlockNumber>,
predicate: impl FnMut(&SealedHeader) -> bool,
) -> ProviderResult<Vec<SealedHeader>> {
self.database.provider()?.sealed_headers_while(range, predicate)
self.database.sealed_headers_while(range, predicate)
}
}
@ -176,7 +179,7 @@ where
Tree: Send + Sync,
{
fn block_hash(&self, number: u64) -> ProviderResult<Option<B256>> {
self.database.provider()?.block_hash(number)
self.database.block_hash(number)
}
fn canonical_hashes_range(
@ -184,7 +187,7 @@ where
start: BlockNumber,
end: BlockNumber,
) -> ProviderResult<Vec<B256>> {
self.database.provider()?.canonical_hashes_range(start, end)
self.database.canonical_hashes_range(start, end)
}
}
@ -202,11 +205,11 @@ where
}
fn last_block_number(&self) -> ProviderResult<BlockNumber> {
self.database.provider()?.last_block_number()
self.database.last_block_number()
}
fn block_number(&self, hash: B256) -> ProviderResult<Option<BlockNumber>> {
self.database.provider()?.block_number(hash)
self.database.block_number(hash)
}
}
@ -237,7 +240,7 @@ where
let block = match source {
BlockSource::Any => {
// check database first
let mut block = self.database.provider()?.block_by_hash(hash)?;
let mut block = self.database.block_by_hash(hash)?;
if block.is_none() {
// Note: it's fine to return the unsealed block because the caller already has
// the hash
@ -246,7 +249,7 @@ where
block
}
BlockSource::Pending => self.tree.block_by_hash(hash).map(|block| block.unseal()),
BlockSource::Database => self.database.provider()?.block_by_hash(hash)?,
BlockSource::Database => self.database.block_by_hash(hash)?,
};
Ok(block)
@ -255,7 +258,7 @@ where
fn block(&self, id: BlockHashOrNumber) -> ProviderResult<Option<Block>> {
match id {
BlockHashOrNumber::Hash(hash) => self.find_block_by_hash(hash, BlockSource::Any),
BlockHashOrNumber::Number(num) => self.database.provider()?.block_by_number(num),
BlockHashOrNumber::Number(num) => self.database.block_by_number(num),
}
}
@ -272,14 +275,14 @@ where
}
fn ommers(&self, id: BlockHashOrNumber) -> ProviderResult<Option<Vec<Header>>> {
self.database.provider()?.ommers(id)
self.database.ommers(id)
}
fn block_body_indices(
&self,
number: BlockNumber,
) -> ProviderResult<Option<StoredBlockBodyIndices>> {
self.database.provider()?.block_body_indices(number)
self.database.block_body_indices(number)
}
/// Returns the block with senders with matching number or hash from database.
@ -293,11 +296,11 @@ where
id: BlockHashOrNumber,
transaction_kind: TransactionVariant,
) -> ProviderResult<Option<BlockWithSenders>> {
self.database.provider()?.block_with_senders(id, transaction_kind)
self.database.block_with_senders(id, transaction_kind)
}
fn block_range(&self, range: RangeInclusive<BlockNumber>) -> ProviderResult<Vec<Block>> {
self.database.provider()?.block_range(range)
self.database.block_range(range)
}
}
@ -307,65 +310,65 @@ where
Tree: BlockchainTreeViewer + Send + Sync,
{
fn transaction_id(&self, tx_hash: TxHash) -> ProviderResult<Option<TxNumber>> {
self.database.provider()?.transaction_id(tx_hash)
self.database.transaction_id(tx_hash)
}
fn transaction_by_id(&self, id: TxNumber) -> ProviderResult<Option<TransactionSigned>> {
self.database.provider()?.transaction_by_id(id)
self.database.transaction_by_id(id)
}
fn transaction_by_id_no_hash(
&self,
id: TxNumber,
) -> ProviderResult<Option<TransactionSignedNoHash>> {
self.database.provider()?.transaction_by_id_no_hash(id)
self.database.transaction_by_id_no_hash(id)
}
fn transaction_by_hash(&self, hash: TxHash) -> ProviderResult<Option<TransactionSigned>> {
self.database.provider()?.transaction_by_hash(hash)
self.database.transaction_by_hash(hash)
}
fn transaction_by_hash_with_meta(
&self,
tx_hash: TxHash,
) -> ProviderResult<Option<(TransactionSigned, TransactionMeta)>> {
self.database.provider()?.transaction_by_hash_with_meta(tx_hash)
self.database.transaction_by_hash_with_meta(tx_hash)
}
fn transaction_block(&self, id: TxNumber) -> ProviderResult<Option<BlockNumber>> {
self.database.provider()?.transaction_block(id)
self.database.transaction_block(id)
}
fn transactions_by_block(
&self,
id: BlockHashOrNumber,
) -> ProviderResult<Option<Vec<TransactionSigned>>> {
self.database.provider()?.transactions_by_block(id)
self.database.transactions_by_block(id)
}
fn transactions_by_block_range(
&self,
range: impl RangeBounds<BlockNumber>,
) -> ProviderResult<Vec<Vec<TransactionSigned>>> {
self.database.provider()?.transactions_by_block_range(range)
self.database.transactions_by_block_range(range)
}
fn transactions_by_tx_range(
&self,
range: impl RangeBounds<TxNumber>,
) -> ProviderResult<Vec<TransactionSignedNoHash>> {
self.database.provider()?.transactions_by_tx_range(range)
self.database.transactions_by_tx_range(range)
}
fn senders_by_tx_range(
&self,
range: impl RangeBounds<TxNumber>,
) -> ProviderResult<Vec<Address>> {
self.database.provider()?.senders_by_tx_range(range)
self.database.senders_by_tx_range(range)
}
fn transaction_sender(&self, id: TxNumber) -> ProviderResult<Option<Address>> {
self.database.provider()?.transaction_sender(id)
self.database.transaction_sender(id)
}
}
@ -375,22 +378,22 @@ where
Tree: Send + Sync,
{
fn receipt(&self, id: TxNumber) -> ProviderResult<Option<Receipt>> {
self.database.provider()?.receipt(id)
self.database.receipt(id)
}
fn receipt_by_hash(&self, hash: TxHash) -> ProviderResult<Option<Receipt>> {
self.database.provider()?.receipt_by_hash(hash)
self.database.receipt_by_hash(hash)
}
fn receipts_by_block(&self, block: BlockHashOrNumber) -> ProviderResult<Option<Vec<Receipt>>> {
self.database.provider()?.receipts_by_block(block)
self.database.receipts_by_block(block)
}
fn receipts_by_tx_range(
&self,
range: impl RangeBounds<TxNumber>,
) -> ProviderResult<Vec<Receipt>> {
self.database.provider()?.receipts_by_tx_range(range)
self.database.receipts_by_tx_range(range)
}
}
impl<DB, Tree> ReceiptProviderIdExt for BlockchainProvider<DB, Tree>
@ -431,11 +434,11 @@ where
id: BlockHashOrNumber,
timestamp: u64,
) -> ProviderResult<Option<Withdrawals>> {
self.database.provider()?.withdrawals_by_block(id, timestamp)
self.database.withdrawals_by_block(id, timestamp)
}
fn latest_withdrawal(&self) -> ProviderResult<Option<Withdrawal>> {
self.database.provider()?.latest_withdrawal()
self.database.latest_withdrawal()
}
}

View File

@ -1,685 +0,0 @@
use super::{LoadedJar, SnapshotJarProvider};
use crate::{
to_range, BlockHashReader, BlockNumReader, BlockReader, BlockSource, HeaderProvider,
ReceiptProvider, TransactionVariant, TransactionsProvider, TransactionsProviderExt,
WithdrawalsProvider,
};
use dashmap::DashMap;
use parking_lot::RwLock;
use reth_db::{
codecs::CompactU256,
models::StoredBlockBodyIndices,
snapshot::{iter_snapshots, HeaderMask, ReceiptMask, SnapshotCursor, TransactionMask},
};
use reth_interfaces::provider::{ProviderError, ProviderResult};
use reth_nippy_jar::NippyJar;
use reth_primitives::{
snapshot::HighestSnapshots, Address, Block, BlockHash, BlockHashOrNumber, BlockNumber,
BlockWithSenders, ChainInfo, Header, Receipt, SealedBlock, SealedBlockWithSenders,
SealedHeader, SnapshotSegment, TransactionMeta, TransactionSigned, TransactionSignedNoHash,
TxHash, TxNumber, Withdrawal, Withdrawals, B256, U256,
};
use std::{
collections::{hash_map::Entry, BTreeMap, HashMap},
ops::{Range, RangeBounds, RangeInclusive},
path::{Path, PathBuf},
};
use tokio::sync::watch;
/// Alias type for a map that can be queried for transaction/block ranges from a block/transaction
/// segment respectively. It uses `BlockNumber` to represent the block end of a snapshot range or
/// `TxNumber` to represent the transaction end of a snapshot range.
///
/// Can be in one of the two formats:
/// - `HashMap<SnapshotSegment, BTreeMap<BlockNumber, RangeInclusive<TxNumber>>>`
/// - `HashMap<SnapshotSegment, BTreeMap<TxNumber, RangeInclusive<BlockNumber>>>`
type SegmentRanges = HashMap<SnapshotSegment, BTreeMap<u64, RangeInclusive<u64>>>;
/// [`SnapshotProvider`] manages all existing [`SnapshotJarProvider`].
#[derive(Debug, Default)]
pub struct SnapshotProvider {
/// Maintains a map which allows for concurrent access to different `NippyJars`, over different
/// segments and ranges.
map: DashMap<(BlockNumber, SnapshotSegment), LoadedJar>,
/// Available snapshot transaction ranges on disk indexed by max blocks.
snapshots_block_index: RwLock<SegmentRanges>,
/// Available snapshot block ranges on disk indexed by max transactions.
snapshots_tx_index: RwLock<SegmentRanges>,
/// Tracks the highest snapshot of every segment.
highest_tracker: Option<watch::Receiver<Option<HighestSnapshots>>>,
/// Directory where snapshots are located
path: PathBuf,
/// Whether [`SnapshotJarProvider`] loads filters into memory. If not, `by_hash` queries won't
/// be able to be queried directly.
load_filters: bool,
}
impl SnapshotProvider {
/// Creates a new [`SnapshotProvider`].
pub fn new(path: impl AsRef<Path>) -> ProviderResult<Self> {
let provider = Self {
map: Default::default(),
snapshots_block_index: Default::default(),
snapshots_tx_index: Default::default(),
highest_tracker: None,
path: path.as_ref().to_path_buf(),
load_filters: false,
};
provider.update_index()?;
Ok(provider)
}
/// Loads filters into memory when creating a [`SnapshotJarProvider`].
pub fn with_filters(mut self) -> Self {
self.load_filters = true;
self
}
/// Adds a highest snapshot tracker to the provider
pub fn with_highest_tracker(
mut self,
highest_tracker: Option<watch::Receiver<Option<HighestSnapshots>>>,
) -> Self {
self.highest_tracker = highest_tracker;
self
}
/// Gets the [`SnapshotJarProvider`] of the requested segment and block.
pub fn get_segment_provider_from_block(
&self,
segment: SnapshotSegment,
block: BlockNumber,
path: Option<&Path>,
) -> ProviderResult<SnapshotJarProvider<'_>> {
self.get_segment_provider(
segment,
|| self.get_segment_ranges_from_block(segment, block),
path,
)?
.ok_or_else(|| ProviderError::MissingSnapshotBlock(segment, block))
}
/// Gets the [`SnapshotJarProvider`] of the requested segment and transaction.
pub fn get_segment_provider_from_transaction(
&self,
segment: SnapshotSegment,
tx: TxNumber,
path: Option<&Path>,
) -> ProviderResult<SnapshotJarProvider<'_>> {
self.get_segment_provider(
segment,
|| self.get_segment_ranges_from_transaction(segment, tx),
path,
)?
.ok_or_else(|| ProviderError::MissingSnapshotTx(segment, tx))
}
/// Gets the [`SnapshotJarProvider`] of the requested segment and block or transaction.
pub fn get_segment_provider(
&self,
segment: SnapshotSegment,
fn_ranges: impl Fn() -> Option<(RangeInclusive<BlockNumber>, RangeInclusive<TxNumber>)>,
path: Option<&Path>,
) -> ProviderResult<Option<SnapshotJarProvider<'_>>> {
// If we have a path, then get the block range and transaction range from its name.
// Otherwise, check `self.available_snapshots`
let snapshot_ranges = match path {
Some(path) => {
SnapshotSegment::parse_filename(path.file_name().ok_or_else(|| {
ProviderError::MissingSnapshotPath(segment, path.to_path_buf())
})?)
.and_then(|(parsed_segment, block_range, tx_range)| {
if parsed_segment == segment {
return Some((block_range, tx_range))
}
None
})
}
None => fn_ranges(),
};
// Return cached `LoadedJar` or insert it for the first time, and then, return it.
if let Some((block_range, tx_range)) = snapshot_ranges {
return Ok(Some(self.get_or_create_jar_provider(segment, &block_range, &tx_range)?))
}
Ok(None)
}
/// Given a segment, block range and transaction range it returns a cached
/// [`SnapshotJarProvider`]. TODO: we should check the size and pop N if there's too many.
fn get_or_create_jar_provider(
&self,
segment: SnapshotSegment,
block_range: &RangeInclusive<u64>,
tx_range: &RangeInclusive<u64>,
) -> ProviderResult<SnapshotJarProvider<'_>> {
let key = (*block_range.end(), segment);
let entry = match self.map.entry(key) {
dashmap::mapref::entry::Entry::Occupied(entry) => entry.into_ref(),
dashmap::mapref::entry::Entry::Vacant(entry) => {
let path = self.path.join(segment.filename(block_range, tx_range));
let mut jar = NippyJar::load(&path)?;
if self.load_filters {
jar.load_filters()?;
}
let loaded_jar = LoadedJar::new(jar)?;
entry.insert(loaded_jar)
}
};
Ok(entry.downgrade().into())
}
/// Gets a snapshot segment's block range and transaction range from the provider inner block
/// index.
fn get_segment_ranges_from_block(
&self,
segment: SnapshotSegment,
block: u64,
) -> Option<(RangeInclusive<BlockNumber>, RangeInclusive<TxNumber>)> {
let snapshots = self.snapshots_block_index.read();
let segment_snapshots = snapshots.get(&segment)?;
// It's more probable that the request comes from a newer block height, so we iterate
// the snapshots in reverse.
let mut snapshots_rev_iter = segment_snapshots.iter().rev().peekable();
while let Some((block_end, tx_range)) = snapshots_rev_iter.next() {
if block > *block_end {
// request block is higher than highest snapshot block
return None
}
// `unwrap_or(0) is safe here as it sets block_start to 0 if the iterator is empty,
// indicating the lowest height snapshot has been reached.
let block_start =
snapshots_rev_iter.peek().map(|(block_end, _)| *block_end + 1).unwrap_or(0);
if block_start <= block {
return Some((block_start..=*block_end, tx_range.clone()))
}
}
None
}
/// Gets a snapshot segment's block range and transaction range from the provider inner
/// transaction index.
fn get_segment_ranges_from_transaction(
&self,
segment: SnapshotSegment,
tx: u64,
) -> Option<(RangeInclusive<BlockNumber>, RangeInclusive<TxNumber>)> {
let snapshots = self.snapshots_tx_index.read();
let segment_snapshots = snapshots.get(&segment)?;
// It's more probable that the request comes from a newer tx height, so we iterate
// the snapshots in reverse.
let mut snapshots_rev_iter = segment_snapshots.iter().rev().peekable();
while let Some((tx_end, block_range)) = snapshots_rev_iter.next() {
if tx > *tx_end {
// request tx is higher than highest snapshot tx
return None
}
let tx_start = snapshots_rev_iter.peek().map(|(tx_end, _)| *tx_end + 1).unwrap_or(0);
if tx_start <= tx {
return Some((block_range.clone(), tx_start..=*tx_end))
}
}
None
}
/// Updates the inner transaction and block index
pub fn update_index(&self) -> ProviderResult<()> {
let mut block_index = self.snapshots_block_index.write();
let mut tx_index = self.snapshots_tx_index.write();
for (segment, ranges) in iter_snapshots(&self.path)? {
for (block_range, tx_range) in ranges {
let block_end = *block_range.end();
let tx_end = *tx_range.end();
match tx_index.entry(segment) {
Entry::Occupied(mut index) => {
index.get_mut().insert(tx_end, block_range);
}
Entry::Vacant(index) => {
index.insert(BTreeMap::from([(tx_end, block_range)]));
}
};
match block_index.entry(segment) {
Entry::Occupied(mut index) => {
index.get_mut().insert(block_end, tx_range);
}
Entry::Vacant(index) => {
index.insert(BTreeMap::from([(block_end, tx_range)]));
}
};
}
}
Ok(())
}
/// Gets the highest snapshot block if it exists for a snapshot segment.
pub fn get_highest_snapshot_block(&self, segment: SnapshotSegment) -> Option<BlockNumber> {
self.snapshots_block_index
.read()
.get(&segment)
.and_then(|index| index.last_key_value().map(|(last_block, _)| *last_block))
}
/// Gets the highest snapshotted transaction.
pub fn get_highest_snapshot_tx(&self, segment: SnapshotSegment) -> Option<TxNumber> {
self.snapshots_tx_index
.read()
.get(&segment)
.and_then(|index| index.last_key_value().map(|(last_tx, _)| *last_tx))
}
/// Iterates through segment snapshots in reverse order, executing a function until it returns
/// some object. Useful for finding objects by [`TxHash`] or [`BlockHash`].
pub fn find_snapshot<T>(
&self,
segment: SnapshotSegment,
func: impl Fn(SnapshotJarProvider<'_>) -> ProviderResult<Option<T>>,
) -> ProviderResult<Option<T>> {
let snapshots = self.snapshots_block_index.read();
if let Some(segment_snapshots) = snapshots.get(&segment) {
// It's more probable that the request comes from a newer block height, so we iterate
// the snapshots in reverse.
let mut snapshots_rev_iter = segment_snapshots.iter().rev().peekable();
while let Some((block_end, tx_range)) = snapshots_rev_iter.next() {
// `unwrap_or(0) is safe here as it sets block_start to 0 if the iterator
// is empty, indicating the lowest height snapshot has been reached.
let block_start =
snapshots_rev_iter.peek().map(|(block_end, _)| *block_end + 1).unwrap_or(0);
if let Some(res) = func(self.get_or_create_jar_provider(
segment,
&(block_start..=*block_end),
tx_range,
)?)? {
return Ok(Some(res))
}
}
}
Ok(None)
}
/// Fetches data within a specified range across multiple snapshot files.
///
/// This function iteratively retrieves data using `get_fn` for each item in the given range.
/// It continues fetching until the end of the range is reached or the provided `predicate`
/// returns false.
pub fn fetch_range<T, F, P>(
&self,
segment: SnapshotSegment,
range: Range<u64>,
get_fn: F,
mut predicate: P,
) -> ProviderResult<Vec<T>>
where
F: Fn(&mut SnapshotCursor<'_>, u64) -> ProviderResult<Option<T>>,
P: FnMut(&T) -> bool,
{
let get_provider = |start: u64| match segment {
SnapshotSegment::Headers => self.get_segment_provider_from_block(segment, start, None),
SnapshotSegment::Transactions | SnapshotSegment::Receipts => {
self.get_segment_provider_from_transaction(segment, start, None)
}
};
let mut result = Vec::with_capacity((range.end - range.start).min(100) as usize);
let mut provider = get_provider(range.start)?;
let mut cursor = provider.cursor()?;
// advances number in range
'outer: for number in range {
// advances snapshot files if `get_fn` returns None
'inner: loop {
match get_fn(&mut cursor, number)? {
Some(res) => {
if !predicate(&res) {
break 'outer
}
result.push(res);
break 'inner
}
None => {
provider = get_provider(number)?;
cursor = provider.cursor()?;
}
}
}
}
Ok(result)
}
}
impl HeaderProvider for SnapshotProvider {
fn header(&self, block_hash: &BlockHash) -> ProviderResult<Option<Header>> {
self.find_snapshot(SnapshotSegment::Headers, |jar_provider| {
Ok(jar_provider
.cursor()?
.get_two::<HeaderMask<Header, BlockHash>>(block_hash.into())?
.and_then(|(header, hash)| {
if &hash == block_hash {
return Some(header)
}
None
}))
})
}
fn header_by_number(&self, num: BlockNumber) -> ProviderResult<Option<Header>> {
self.get_segment_provider_from_block(SnapshotSegment::Headers, num, None)?
.header_by_number(num)
}
fn header_td(&self, block_hash: &BlockHash) -> ProviderResult<Option<U256>> {
self.find_snapshot(SnapshotSegment::Headers, |jar_provider| {
Ok(jar_provider
.cursor()?
.get_two::<HeaderMask<CompactU256, BlockHash>>(block_hash.into())?
.and_then(|(td, hash)| (&hash == block_hash).then_some(td.0)))
})
}
fn header_td_by_number(&self, num: BlockNumber) -> ProviderResult<Option<U256>> {
self.get_segment_provider_from_block(SnapshotSegment::Headers, num, None)?
.header_td_by_number(num)
}
fn headers_range(&self, range: impl RangeBounds<BlockNumber>) -> ProviderResult<Vec<Header>> {
self.fetch_range(
SnapshotSegment::Headers,
to_range(range),
|cursor, number| cursor.get_one::<HeaderMask<Header>>(number.into()),
|_| true,
)
}
fn sealed_header(&self, num: BlockNumber) -> ProviderResult<Option<SealedHeader>> {
self.get_segment_provider_from_block(SnapshotSegment::Headers, num, None)?
.sealed_header(num)
}
fn sealed_headers_while(
&self,
range: impl RangeBounds<BlockNumber>,
predicate: impl FnMut(&SealedHeader) -> bool,
) -> ProviderResult<Vec<SealedHeader>> {
self.fetch_range(
SnapshotSegment::Headers,
to_range(range),
|cursor, number| {
Ok(cursor
.get_two::<HeaderMask<Header, BlockHash>>(number.into())?
.map(|(header, hash)| header.seal(hash)))
},
predicate,
)
}
}
impl BlockHashReader for SnapshotProvider {
fn block_hash(&self, num: u64) -> ProviderResult<Option<B256>> {
self.get_segment_provider_from_block(SnapshotSegment::Headers, num, None)?.block_hash(num)
}
fn canonical_hashes_range(
&self,
start: BlockNumber,
end: BlockNumber,
) -> ProviderResult<Vec<B256>> {
self.fetch_range(
SnapshotSegment::Headers,
start..end,
|cursor, number| cursor.get_one::<HeaderMask<BlockHash>>(number.into()),
|_| true,
)
}
}
impl ReceiptProvider for SnapshotProvider {
fn receipt(&self, num: TxNumber) -> ProviderResult<Option<Receipt>> {
self.get_segment_provider_from_transaction(SnapshotSegment::Receipts, num, None)?
.receipt(num)
}
fn receipt_by_hash(&self, hash: TxHash) -> ProviderResult<Option<Receipt>> {
if let Some(num) = self.transaction_id(hash)? {
return self.receipt(num)
}
Ok(None)
}
fn receipts_by_block(&self, _block: BlockHashOrNumber) -> ProviderResult<Option<Vec<Receipt>>> {
unreachable!()
}
fn receipts_by_tx_range(
&self,
range: impl RangeBounds<TxNumber>,
) -> ProviderResult<Vec<Receipt>> {
self.fetch_range(
SnapshotSegment::Receipts,
to_range(range),
|cursor, number| cursor.get_one::<ReceiptMask<Receipt>>(number.into()),
|_| true,
)
}
}
impl TransactionsProviderExt for SnapshotProvider {
fn transaction_hashes_by_range(
&self,
tx_range: Range<TxNumber>,
) -> ProviderResult<Vec<(TxHash, TxNumber)>> {
self.fetch_range(
SnapshotSegment::Transactions,
tx_range,
|cursor, number| {
let tx =
cursor.get_one::<TransactionMask<TransactionSignedNoHash>>(number.into())?;
Ok(tx.map(|tx| (tx.hash(), cursor.number())))
},
|_| true,
)
}
}
impl TransactionsProvider for SnapshotProvider {
fn transaction_id(&self, tx_hash: TxHash) -> ProviderResult<Option<TxNumber>> {
self.find_snapshot(SnapshotSegment::Transactions, |jar_provider| {
let mut cursor = jar_provider.cursor()?;
if cursor
.get_one::<TransactionMask<TransactionSignedNoHash>>((&tx_hash).into())?
.and_then(|tx| (tx.hash() == tx_hash).then_some(tx))
.is_some()
{
Ok(Some(cursor.number()))
} else {
Ok(None)
}
})
}
fn transaction_by_id(&self, num: TxNumber) -> ProviderResult<Option<TransactionSigned>> {
self.get_segment_provider_from_transaction(SnapshotSegment::Transactions, num, None)?
.transaction_by_id(num)
}
fn transaction_by_id_no_hash(
&self,
num: TxNumber,
) -> ProviderResult<Option<TransactionSignedNoHash>> {
self.get_segment_provider_from_transaction(SnapshotSegment::Transactions, num, None)?
.transaction_by_id_no_hash(num)
}
fn transaction_by_hash(&self, hash: TxHash) -> ProviderResult<Option<TransactionSigned>> {
self.find_snapshot(SnapshotSegment::Transactions, |jar_provider| {
Ok(jar_provider
.cursor()?
.get_one::<TransactionMask<TransactionSignedNoHash>>((&hash).into())?
.map(|tx| tx.with_hash())
.and_then(|tx| (tx.hash_ref() == &hash).then_some(tx)))
})
}
fn transaction_by_hash_with_meta(
&self,
_hash: TxHash,
) -> ProviderResult<Option<(TransactionSigned, TransactionMeta)>> {
// Required data not present in snapshots
Err(ProviderError::UnsupportedProvider)
}
fn transaction_block(&self, _id: TxNumber) -> ProviderResult<Option<BlockNumber>> {
// Required data not present in snapshots
Err(ProviderError::UnsupportedProvider)
}
fn transactions_by_block(
&self,
_block_id: BlockHashOrNumber,
) -> ProviderResult<Option<Vec<TransactionSigned>>> {
// Required data not present in snapshots
Err(ProviderError::UnsupportedProvider)
}
fn transactions_by_block_range(
&self,
_range: impl RangeBounds<BlockNumber>,
) -> ProviderResult<Vec<Vec<TransactionSigned>>> {
// Required data not present in snapshots
Err(ProviderError::UnsupportedProvider)
}
fn senders_by_tx_range(
&self,
range: impl RangeBounds<TxNumber>,
) -> ProviderResult<Vec<Address>> {
let txes = self.transactions_by_tx_range(range)?;
TransactionSignedNoHash::recover_signers(&txes, txes.len())
.ok_or(ProviderError::SenderRecoveryError)
}
fn transactions_by_tx_range(
&self,
range: impl RangeBounds<TxNumber>,
) -> ProviderResult<Vec<reth_primitives::TransactionSignedNoHash>> {
self.fetch_range(
SnapshotSegment::Transactions,
to_range(range),
|cursor, number| {
cursor.get_one::<TransactionMask<TransactionSignedNoHash>>(number.into())
},
|_| true,
)
}
fn transaction_sender(&self, id: TxNumber) -> ProviderResult<Option<Address>> {
Ok(self.transaction_by_id_no_hash(id)?.and_then(|tx| tx.recover_signer()))
}
}
/* Cannot be successfully implemented but must exist for trait requirements */
impl BlockNumReader for SnapshotProvider {
fn chain_info(&self) -> ProviderResult<ChainInfo> {
// Required data not present in snapshots
Err(ProviderError::UnsupportedProvider)
}
fn best_block_number(&self) -> ProviderResult<BlockNumber> {
// Required data not present in snapshots
Err(ProviderError::UnsupportedProvider)
}
fn last_block_number(&self) -> ProviderResult<BlockNumber> {
// Required data not present in snapshots
Err(ProviderError::UnsupportedProvider)
}
fn block_number(&self, _hash: B256) -> ProviderResult<Option<BlockNumber>> {
// Required data not present in snapshots
Err(ProviderError::UnsupportedProvider)
}
}
impl BlockReader for SnapshotProvider {
fn find_block_by_hash(
&self,
_hash: B256,
_source: BlockSource,
) -> ProviderResult<Option<Block>> {
// Required data not present in snapshots
Err(ProviderError::UnsupportedProvider)
}
fn block(&self, _id: BlockHashOrNumber) -> ProviderResult<Option<Block>> {
// Required data not present in snapshots
Err(ProviderError::UnsupportedProvider)
}
fn pending_block(&self) -> ProviderResult<Option<SealedBlock>> {
// Required data not present in snapshots
Err(ProviderError::UnsupportedProvider)
}
fn pending_block_with_senders(&self) -> ProviderResult<Option<SealedBlockWithSenders>> {
// Required data not present in snapshots
Err(ProviderError::UnsupportedProvider)
}
fn pending_block_and_receipts(&self) -> ProviderResult<Option<(SealedBlock, Vec<Receipt>)>> {
// Required data not present in snapshots
Err(ProviderError::UnsupportedProvider)
}
fn ommers(&self, _id: BlockHashOrNumber) -> ProviderResult<Option<Vec<Header>>> {
// Required data not present in snapshots
Err(ProviderError::UnsupportedProvider)
}
fn block_body_indices(&self, _num: u64) -> ProviderResult<Option<StoredBlockBodyIndices>> {
// Required data not present in snapshots
Err(ProviderError::UnsupportedProvider)
}
fn block_with_senders(
&self,
_id: BlockHashOrNumber,
_transaction_kind: TransactionVariant,
) -> ProviderResult<Option<BlockWithSenders>> {
// Required data not present in snapshots
Err(ProviderError::UnsupportedProvider)
}
fn block_range(&self, _range: RangeInclusive<BlockNumber>) -> ProviderResult<Vec<Block>> {
// Required data not present in snapshots
Err(ProviderError::UnsupportedProvider)
}
}
impl WithdrawalsProvider for SnapshotProvider {
fn withdrawals_by_block(
&self,
_id: BlockHashOrNumber,
_timestamp: u64,
) -> ProviderResult<Option<Withdrawals>> {
// Required data not present in snapshots
Err(ProviderError::UnsupportedProvider)
}
fn latest_withdrawal(&self) -> ProviderResult<Option<Withdrawal>> {
// Required data not present in snapshots
Err(ProviderError::UnsupportedProvider)
}
}

View File

@ -1,6 +1,7 @@
use crate::{
providers::state::macros::delegate_provider_impls, AccountReader, BlockHashReader,
BundleStateWithReceipts, ProviderError, StateProvider, StateRootProvider,
providers::{state::macros::delegate_provider_impls, StaticFileProvider},
AccountReader, BlockHashReader, BundleStateWithReceipts, ProviderError, StateProvider,
StateRootProvider,
};
use reth_db::{
cursor::{DbCursorRO, DbDupCursorRO},
@ -13,9 +14,10 @@ use reth_db::{
use reth_interfaces::provider::ProviderResult;
use reth_primitives::{
constants::EPOCH_SLOTS, trie::AccountProof, Account, Address, BlockNumber, Bytecode,
StorageKey, StorageValue, B256,
StaticFileSegment, StorageKey, StorageValue, B256,
};
use reth_trie::{updates::TrieUpdates, HashedPostState};
use std::fmt::Debug;
/// State provider for a given block number which takes a tx reference.
///
@ -23,11 +25,11 @@ use reth_trie::{updates::TrieUpdates, HashedPostState};
/// It means that all changes made in the provided block number are not included.
///
/// Historical state provider reads the following tables:
/// - [tables::AccountHistory]
/// - [tables::AccountsHistory]
/// - [tables::Bytecodes]
/// - [tables::StorageHistory]
/// - [tables::AccountChangeSet]
/// - [tables::StorageChangeSet]
/// - [tables::StoragesHistory]
/// - [tables::AccountChangeSets]
/// - [tables::StorageChangeSets]
#[derive(Debug)]
pub struct HistoricalStateProviderRef<'b, TX: DbTx> {
/// Transaction
@ -36,6 +38,8 @@ pub struct HistoricalStateProviderRef<'b, TX: DbTx> {
block_number: BlockNumber,
/// Lowest blocks at which different parts of the state are available.
lowest_available_blocks: LowestAvailableBlocks,
/// Static File provider
static_file_provider: StaticFileProvider,
}
#[derive(Debug, Eq, PartialEq)]
@ -48,8 +52,12 @@ pub enum HistoryInfo {
impl<'b, TX: DbTx> HistoricalStateProviderRef<'b, TX> {
/// Create new StateProvider for historical block number
pub fn new(tx: &'b TX, block_number: BlockNumber) -> Self {
Self { tx, block_number, lowest_available_blocks: Default::default() }
pub fn new(
tx: &'b TX,
block_number: BlockNumber,
static_file_provider: StaticFileProvider,
) -> Self {
Self { tx, block_number, lowest_available_blocks: Default::default(), static_file_provider }
}
/// Create new StateProvider for historical block number and lowest block numbers at which
@ -58,11 +66,12 @@ impl<'b, TX: DbTx> HistoricalStateProviderRef<'b, TX> {
tx: &'b TX,
block_number: BlockNumber,
lowest_available_blocks: LowestAvailableBlocks,
static_file_provider: StaticFileProvider,
) -> Self {
Self { tx, block_number, lowest_available_blocks }
Self { tx, block_number, lowest_available_blocks, static_file_provider }
}
/// Lookup an account in the AccountHistory table
/// Lookup an account in the AccountsHistory table
pub fn account_history_lookup(&self, address: Address) -> ProviderResult<HistoryInfo> {
if !self.lowest_available_blocks.is_account_history_available(self.block_number) {
return Err(ProviderError::StateAtBlockPruned(self.block_number))
@ -70,14 +79,14 @@ impl<'b, TX: DbTx> HistoricalStateProviderRef<'b, TX> {
// history key to search IntegerList of block number changesets.
let history_key = ShardedKey::new(address, self.block_number);
self.history_info::<tables::AccountHistory, _>(
self.history_info::<tables::AccountsHistory, _>(
history_key,
|key| key.key == address,
self.lowest_available_blocks.account_history_block_number,
)
}
/// Lookup a storage key in the StorageHistory table
/// Lookup a storage key in the StoragesHistory table
pub fn storage_history_lookup(
&self,
address: Address,
@ -89,7 +98,7 @@ impl<'b, TX: DbTx> HistoricalStateProviderRef<'b, TX> {
// history key to search IntegerList of block number changesets.
let history_key = StorageShardedKey::new(address, storage_key, self.block_number);
self.history_info::<tables::StorageHistory, _>(
self.history_info::<tables::StoragesHistory, _>(
history_key,
|key| key.address == address && key.sharded_key.key == storage_key,
self.lowest_available_blocks.storage_history_block_number,
@ -104,10 +113,14 @@ impl<'b, TX: DbTx> HistoricalStateProviderRef<'b, TX> {
return Err(ProviderError::StateAtBlockPruned(self.block_number))
}
let (tip, _) = self
let tip = self
.tx
.cursor_read::<tables::CanonicalHeaders>()?
.last()?
.map(|(tip, _)| tip)
.or_else(|| {
self.static_file_provider.get_highest_static_file_block(StaticFileSegment::Headers)
})
.ok_or(ProviderError::BestBlockNotFound)?;
if tip.saturating_sub(self.block_number) > EPOCH_SLOTS {
@ -136,10 +149,16 @@ impl<'b, TX: DbTx> HistoricalStateProviderRef<'b, TX> {
// index, the first chunk for the next key will be returned so we filter out chunks that
// have a different key.
if let Some(chunk) = cursor.seek(key)?.filter(|(key, _)| key_filter(key)).map(|x| x.1 .0) {
let chunk = chunk.enable_rank();
// Get the rank of the first entry before or equal to our block.
let mut rank = chunk.rank(self.block_number);
// Get the rank of the first entry after our block.
let rank = chunk.rank(self.block_number as usize);
// Adjust the rank, so that we have the rank of the first entry strictly before our
// block (not equal to it).
if rank.checked_sub(1).and_then(|rank| chunk.select(rank)) == Some(self.block_number) {
rank -= 1
};
let block_number = chunk.select(rank);
// If our block is before the first entry in the index chunk and this first entry
// doesn't equal to our block, it might be before the first write ever. To check, we
@ -148,20 +167,21 @@ impl<'b, TX: DbTx> HistoricalStateProviderRef<'b, TX> {
// short-circuit) and when it passes we save a full seek into the changeset/plain state
// table.
if rank == 0 &&
chunk.select(rank) as u64 != self.block_number &&
block_number != Some(self.block_number) &&
!cursor.prev()?.is_some_and(|(key, _)| key_filter(&key))
{
if lowest_available_block_number.is_some() {
if let (Some(_), Some(block_number)) = (lowest_available_block_number, block_number)
{
// The key may have been written, but due to pruning we may not have changesets
// and history, so we need to make a changeset lookup.
Ok(HistoryInfo::InChangeset(chunk.select(rank) as u64))
Ok(HistoryInfo::InChangeset(block_number))
} else {
// The key is written to, but only after our block.
Ok(HistoryInfo::NotYetWritten)
}
} else if rank < chunk.len() {
} else if let Some(block_number) = block_number {
// The chunk contains an entry for a write after our block, return it.
Ok(HistoryInfo::InChangeset(chunk.select(rank) as u64))
Ok(HistoryInfo::InChangeset(block_number))
} else {
// The chunk does not contain an entry for a write after our block. This can only
// happen if this is the last chunk and so we need to look in the plain state.
@ -185,7 +205,7 @@ impl<'b, TX: DbTx> AccountReader for HistoricalStateProviderRef<'b, TX> {
HistoryInfo::NotYetWritten => Ok(None),
HistoryInfo::InChangeset(changeset_block_number) => Ok(self
.tx
.cursor_dup_read::<tables::AccountChangeSet>()?
.cursor_dup_read::<tables::AccountChangeSets>()?
.seek_by_key_subkey(changeset_block_number, address)?
.filter(|acc| acc.address == address)
.ok_or(ProviderError::AccountChangesetNotFound {
@ -203,7 +223,12 @@ impl<'b, TX: DbTx> AccountReader for HistoricalStateProviderRef<'b, TX> {
impl<'b, TX: DbTx> BlockHashReader for HistoricalStateProviderRef<'b, TX> {
/// Get block hash by number.
fn block_hash(&self, number: u64) -> ProviderResult<Option<B256>> {
self.tx.get::<tables::CanonicalHeaders>(number).map_err(Into::into)
self.static_file_provider.get_with_static_file_or_database(
StaticFileSegment::Headers,
number,
|static_file| static_file.block_hash(number),
|| Ok(self.tx.get::<tables::CanonicalHeaders>(number)?),
)
}
fn canonical_hashes_range(
@ -211,16 +236,23 @@ impl<'b, TX: DbTx> BlockHashReader for HistoricalStateProviderRef<'b, TX> {
start: BlockNumber,
end: BlockNumber,
) -> ProviderResult<Vec<B256>> {
let range = start..end;
self.tx
.cursor_read::<tables::CanonicalHeaders>()
.map(|mut cursor| {
cursor
.walk_range(range)?
.map(|result| result.map(|(_, hash)| hash).map_err(Into::into))
.collect::<ProviderResult<Vec<_>>>()
})?
.map_err(Into::into)
self.static_file_provider.get_range_with_static_file_or_database(
StaticFileSegment::Headers,
start..end,
|static_file, range, _| static_file.canonical_hashes_range(range.start, range.end),
|range, _| {
self.tx
.cursor_read::<tables::CanonicalHeaders>()
.map(|mut cursor| {
cursor
.walk_range(range)?
.map(|result| result.map(|(_, hash)| hash).map_err(Into::into))
.collect::<ProviderResult<Vec<_>>>()
})?
.map_err(Into::into)
},
|_| true,
)
}
}
@ -254,7 +286,7 @@ impl<'b, TX: DbTx> StateProvider for HistoricalStateProviderRef<'b, TX> {
HistoryInfo::NotYetWritten => Ok(None),
HistoryInfo::InChangeset(changeset_block_number) => Ok(Some(
self.tx
.cursor_dup_read::<tables::StorageChangeSet>()?
.cursor_dup_read::<tables::StorageChangeSets>()?
.seek_by_key_subkey((changeset_block_number, address).into(), storage_key)?
.filter(|entry| entry.key == storage_key)
.ok_or_else(|| ProviderError::StorageChangesetNotFound {
@ -295,12 +327,18 @@ pub struct HistoricalStateProvider<TX: DbTx> {
block_number: BlockNumber,
/// Lowest blocks at which different parts of the state are available.
lowest_available_blocks: LowestAvailableBlocks,
/// Static File provider
static_file_provider: StaticFileProvider,
}
impl<TX: DbTx> HistoricalStateProvider<TX> {
/// Create new StateProvider for historical block number
pub fn new(tx: TX, block_number: BlockNumber) -> Self {
Self { tx, block_number, lowest_available_blocks: Default::default() }
pub fn new(
tx: TX,
block_number: BlockNumber,
static_file_provider: StaticFileProvider,
) -> Self {
Self { tx, block_number, lowest_available_blocks: Default::default(), static_file_provider }
}
/// Set the lowest block number at which the account history is available.
@ -328,6 +366,7 @@ impl<TX: DbTx> HistoricalStateProvider<TX> {
&self.tx,
self.block_number,
self.lowest_available_blocks,
self.static_file_provider.clone(),
)
}
}
@ -367,13 +406,12 @@ impl LowestAvailableBlocks {
mod tests {
use crate::{
providers::state::historical::{HistoryInfo, LowestAvailableBlocks},
test_utils::create_test_provider_factory,
AccountReader, HistoricalStateProvider, HistoricalStateProviderRef, StateProvider,
};
use reth_db::{
database::Database,
models::{storage_sharded_key::StorageShardedKey, AccountBeforeTx, ShardedKey},
tables,
test_utils::create_test_rw_db,
transaction::{DbTx, DbTxMut},
BlockNumberList,
};
@ -392,20 +430,21 @@ mod tests {
#[test]
fn history_provider_get_account() {
let db = create_test_rw_db();
let tx = db.tx_mut().unwrap();
let factory = create_test_provider_factory();
let tx = factory.provider_rw().unwrap().into_tx();
let static_file_provider = factory.static_file_provider();
tx.put::<tables::AccountHistory>(
tx.put::<tables::AccountsHistory>(
ShardedKey { key: ADDRESS, highest_block_number: 7 },
BlockNumberList::new([1, 3, 7]).unwrap(),
)
.unwrap();
tx.put::<tables::AccountHistory>(
tx.put::<tables::AccountsHistory>(
ShardedKey { key: ADDRESS, highest_block_number: u64::MAX },
BlockNumberList::new([10, 15]).unwrap(),
)
.unwrap();
tx.put::<tables::AccountHistory>(
tx.put::<tables::AccountsHistory>(
ShardedKey { key: HIGHER_ADDRESS, highest_block_number: u64::MAX },
BlockNumberList::new([4]).unwrap(),
)
@ -420,29 +459,29 @@ mod tests {
let higher_acc_plain = Account { nonce: 4, balance: U256::ZERO, bytecode_hash: None };
// setup
tx.put::<tables::AccountChangeSet>(1, AccountBeforeTx { address: ADDRESS, info: None })
tx.put::<tables::AccountChangeSets>(1, AccountBeforeTx { address: ADDRESS, info: None })
.unwrap();
tx.put::<tables::AccountChangeSet>(
tx.put::<tables::AccountChangeSets>(
3,
AccountBeforeTx { address: ADDRESS, info: Some(acc_at3) },
)
.unwrap();
tx.put::<tables::AccountChangeSet>(
tx.put::<tables::AccountChangeSets>(
4,
AccountBeforeTx { address: HIGHER_ADDRESS, info: None },
)
.unwrap();
tx.put::<tables::AccountChangeSet>(
tx.put::<tables::AccountChangeSets>(
7,
AccountBeforeTx { address: ADDRESS, info: Some(acc_at7) },
)
.unwrap();
tx.put::<tables::AccountChangeSet>(
tx.put::<tables::AccountChangeSets>(
10,
AccountBeforeTx { address: ADDRESS, info: Some(acc_at10) },
)
.unwrap();
tx.put::<tables::AccountChangeSet>(
tx.put::<tables::AccountChangeSets>(
15,
AccountBeforeTx { address: ADDRESS, info: Some(acc_at15) },
)
@ -453,56 +492,75 @@ mod tests {
tx.put::<tables::PlainAccountState>(HIGHER_ADDRESS, higher_acc_plain).unwrap();
tx.commit().unwrap();
let tx = db.tx().unwrap();
let tx = factory.provider().unwrap().into_tx();
// run
assert_eq!(HistoricalStateProviderRef::new(&tx, 1).basic_account(ADDRESS), Ok(None));
assert_eq!(
HistoricalStateProviderRef::new(&tx, 2).basic_account(ADDRESS),
HistoricalStateProviderRef::new(&tx, 1, static_file_provider.clone())
.basic_account(ADDRESS)
.clone(),
Ok(None)
);
assert_eq!(
HistoricalStateProviderRef::new(&tx, 2, static_file_provider.clone())
.basic_account(ADDRESS),
Ok(Some(acc_at3))
);
assert_eq!(
HistoricalStateProviderRef::new(&tx, 3).basic_account(ADDRESS),
HistoricalStateProviderRef::new(&tx, 3, static_file_provider.clone())
.basic_account(ADDRESS),
Ok(Some(acc_at3))
);
assert_eq!(
HistoricalStateProviderRef::new(&tx, 4).basic_account(ADDRESS),
HistoricalStateProviderRef::new(&tx, 4, static_file_provider.clone())
.basic_account(ADDRESS),
Ok(Some(acc_at7))
);
assert_eq!(
HistoricalStateProviderRef::new(&tx, 7).basic_account(ADDRESS),
HistoricalStateProviderRef::new(&tx, 7, static_file_provider.clone())
.basic_account(ADDRESS),
Ok(Some(acc_at7))
);
assert_eq!(
HistoricalStateProviderRef::new(&tx, 9).basic_account(ADDRESS),
HistoricalStateProviderRef::new(&tx, 9, static_file_provider.clone())
.basic_account(ADDRESS),
Ok(Some(acc_at10))
);
assert_eq!(
HistoricalStateProviderRef::new(&tx, 10).basic_account(ADDRESS),
HistoricalStateProviderRef::new(&tx, 10, static_file_provider.clone())
.basic_account(ADDRESS),
Ok(Some(acc_at10))
);
assert_eq!(
HistoricalStateProviderRef::new(&tx, 11).basic_account(ADDRESS),
HistoricalStateProviderRef::new(&tx, 11, static_file_provider.clone())
.basic_account(ADDRESS),
Ok(Some(acc_at15))
);
assert_eq!(
HistoricalStateProviderRef::new(&tx, 16).basic_account(ADDRESS),
HistoricalStateProviderRef::new(&tx, 16, static_file_provider.clone())
.basic_account(ADDRESS),
Ok(Some(acc_plain))
);
assert_eq!(HistoricalStateProviderRef::new(&tx, 1).basic_account(HIGHER_ADDRESS), Ok(None));
assert_eq!(
HistoricalStateProviderRef::new(&tx, 1000).basic_account(HIGHER_ADDRESS),
HistoricalStateProviderRef::new(&tx, 1, static_file_provider.clone())
.basic_account(HIGHER_ADDRESS),
Ok(None)
);
assert_eq!(
HistoricalStateProviderRef::new(&tx, 1000, static_file_provider.clone())
.basic_account(HIGHER_ADDRESS),
Ok(Some(higher_acc_plain))
);
}
#[test]
fn history_provider_get_storage() {
let db = create_test_rw_db();
let tx = db.tx_mut().unwrap();
let factory = create_test_provider_factory();
let tx = factory.provider_rw().unwrap().into_tx();
let static_file_provider = factory.static_file_provider();
tx.put::<tables::StorageHistory>(
tx.put::<tables::StoragesHistory>(
StorageShardedKey {
address: ADDRESS,
sharded_key: ShardedKey { key: STORAGE, highest_block_number: 7 },
@ -510,7 +568,7 @@ mod tests {
BlockNumberList::new([3, 7]).unwrap(),
)
.unwrap();
tx.put::<tables::StorageHistory>(
tx.put::<tables::StoragesHistory>(
StorageShardedKey {
address: ADDRESS,
sharded_key: ShardedKey { key: STORAGE, highest_block_number: u64::MAX },
@ -518,7 +576,7 @@ mod tests {
BlockNumberList::new([10, 15]).unwrap(),
)
.unwrap();
tx.put::<tables::StorageHistory>(
tx.put::<tables::StoragesHistory>(
StorageShardedKey {
address: HIGHER_ADDRESS,
sharded_key: ShardedKey { key: STORAGE, highest_block_number: u64::MAX },
@ -536,63 +594,77 @@ mod tests {
let entry_at3 = StorageEntry { key: STORAGE, value: U256::from(0) };
// setup
tx.put::<tables::StorageChangeSet>((3, ADDRESS).into(), entry_at3).unwrap();
tx.put::<tables::StorageChangeSet>((4, HIGHER_ADDRESS).into(), higher_entry_at4).unwrap();
tx.put::<tables::StorageChangeSet>((7, ADDRESS).into(), entry_at7).unwrap();
tx.put::<tables::StorageChangeSet>((10, ADDRESS).into(), entry_at10).unwrap();
tx.put::<tables::StorageChangeSet>((15, ADDRESS).into(), entry_at15).unwrap();
tx.put::<tables::StorageChangeSets>((3, ADDRESS).into(), entry_at3).unwrap();
tx.put::<tables::StorageChangeSets>((4, HIGHER_ADDRESS).into(), higher_entry_at4).unwrap();
tx.put::<tables::StorageChangeSets>((7, ADDRESS).into(), entry_at7).unwrap();
tx.put::<tables::StorageChangeSets>((10, ADDRESS).into(), entry_at10).unwrap();
tx.put::<tables::StorageChangeSets>((15, ADDRESS).into(), entry_at15).unwrap();
// setup plain state
tx.put::<tables::PlainStorageState>(ADDRESS, entry_plain).unwrap();
tx.put::<tables::PlainStorageState>(HIGHER_ADDRESS, higher_entry_plain).unwrap();
tx.commit().unwrap();
let tx = db.tx().unwrap();
let tx = factory.provider().unwrap().into_tx();
// run
assert_eq!(HistoricalStateProviderRef::new(&tx, 0).storage(ADDRESS, STORAGE), Ok(None));
assert_eq!(
HistoricalStateProviderRef::new(&tx, 3).storage(ADDRESS, STORAGE),
Ok(Some(U256::ZERO))
);
assert_eq!(
HistoricalStateProviderRef::new(&tx, 4).storage(ADDRESS, STORAGE),
Ok(Some(entry_at7.value))
);
assert_eq!(
HistoricalStateProviderRef::new(&tx, 7).storage(ADDRESS, STORAGE),
Ok(Some(entry_at7.value))
);
assert_eq!(
HistoricalStateProviderRef::new(&tx, 9).storage(ADDRESS, STORAGE),
Ok(Some(entry_at10.value))
);
assert_eq!(
HistoricalStateProviderRef::new(&tx, 10).storage(ADDRESS, STORAGE),
Ok(Some(entry_at10.value))
);
assert_eq!(
HistoricalStateProviderRef::new(&tx, 11).storage(ADDRESS, STORAGE),
Ok(Some(entry_at15.value))
);
assert_eq!(
HistoricalStateProviderRef::new(&tx, 16).storage(ADDRESS, STORAGE),
Ok(Some(entry_plain.value))
);
assert_eq!(
HistoricalStateProviderRef::new(&tx, 1).storage(HIGHER_ADDRESS, STORAGE),
HistoricalStateProviderRef::new(&tx, 0, static_file_provider.clone())
.storage(ADDRESS, STORAGE),
Ok(None)
);
assert_eq!(
HistoricalStateProviderRef::new(&tx, 1000).storage(HIGHER_ADDRESS, STORAGE),
HistoricalStateProviderRef::new(&tx, 3, static_file_provider.clone())
.storage(ADDRESS, STORAGE),
Ok(Some(U256::ZERO))
);
assert_eq!(
HistoricalStateProviderRef::new(&tx, 4, static_file_provider.clone())
.storage(ADDRESS, STORAGE),
Ok(Some(entry_at7.value))
);
assert_eq!(
HistoricalStateProviderRef::new(&tx, 7, static_file_provider.clone())
.storage(ADDRESS, STORAGE),
Ok(Some(entry_at7.value))
);
assert_eq!(
HistoricalStateProviderRef::new(&tx, 9, static_file_provider.clone())
.storage(ADDRESS, STORAGE),
Ok(Some(entry_at10.value))
);
assert_eq!(
HistoricalStateProviderRef::new(&tx, 10, static_file_provider.clone())
.storage(ADDRESS, STORAGE),
Ok(Some(entry_at10.value))
);
assert_eq!(
HistoricalStateProviderRef::new(&tx, 11, static_file_provider.clone())
.storage(ADDRESS, STORAGE),
Ok(Some(entry_at15.value))
);
assert_eq!(
HistoricalStateProviderRef::new(&tx, 16, static_file_provider.clone())
.storage(ADDRESS, STORAGE),
Ok(Some(entry_plain.value))
);
assert_eq!(
HistoricalStateProviderRef::new(&tx, 1, static_file_provider.clone())
.storage(HIGHER_ADDRESS, STORAGE),
Ok(None)
);
assert_eq!(
HistoricalStateProviderRef::new(&tx, 1000, static_file_provider)
.storage(HIGHER_ADDRESS, STORAGE),
Ok(Some(higher_entry_plain.value))
);
}
#[test]
fn history_provider_unavailable() {
let db = create_test_rw_db();
let tx = db.tx().unwrap();
let factory = create_test_provider_factory();
let tx = factory.provider_rw().unwrap().into_tx();
let static_file_provider = factory.static_file_provider();
// provider block_number < lowest available block number,
// i.e. state at provider block is pruned
@ -603,6 +675,7 @@ mod tests {
account_history_block_number: Some(3),
storage_history_block_number: Some(3),
},
static_file_provider.clone(),
);
assert_eq!(
provider.account_history_lookup(ADDRESS),
@ -622,6 +695,7 @@ mod tests {
account_history_block_number: Some(2),
storage_history_block_number: Some(2),
},
static_file_provider.clone(),
);
assert_eq!(provider.account_history_lookup(ADDRESS), Ok(HistoryInfo::MaybeInPlainState));
assert_eq!(
@ -638,6 +712,7 @@ mod tests {
account_history_block_number: Some(1),
storage_history_block_number: Some(1),
},
static_file_provider.clone(),
);
assert_eq!(provider.account_history_lookup(ADDRESS), Ok(HistoryInfo::MaybeInPlainState));
assert_eq!(

View File

@ -1,6 +1,6 @@
use crate::{
providers::state::macros::delegate_provider_impls, AccountReader, BlockHashReader,
BundleStateWithReceipts, StateProvider, StateRootProvider,
providers::{state::macros::delegate_provider_impls, StaticFileProvider},
AccountReader, BlockHashReader, BundleStateWithReceipts, StateProvider, StateRootProvider,
};
use reth_db::{
cursor::{DbCursorRO, DbDupCursorRO},
@ -9,7 +9,8 @@ use reth_db::{
};
use reth_interfaces::provider::{ProviderError, ProviderResult};
use reth_primitives::{
trie::AccountProof, Account, Address, BlockNumber, Bytecode, StorageKey, StorageValue, B256,
trie::AccountProof, Account, Address, BlockNumber, Bytecode, StaticFileSegment, StorageKey,
StorageValue, B256,
};
use reth_trie::{proof::Proof, updates::TrieUpdates};
@ -18,12 +19,14 @@ use reth_trie::{proof::Proof, updates::TrieUpdates};
pub struct LatestStateProviderRef<'b, TX: DbTx> {
/// database transaction
db: &'b TX,
/// Static File provider
static_file_provider: StaticFileProvider,
}
impl<'b, TX: DbTx> LatestStateProviderRef<'b, TX> {
/// Create new state provider
pub fn new(db: &'b TX) -> Self {
Self { db }
pub fn new(db: &'b TX, static_file_provider: StaticFileProvider) -> Self {
Self { db, static_file_provider }
}
}
@ -37,7 +40,12 @@ impl<'b, TX: DbTx> AccountReader for LatestStateProviderRef<'b, TX> {
impl<'b, TX: DbTx> BlockHashReader for LatestStateProviderRef<'b, TX> {
/// Get block hash by number.
fn block_hash(&self, number: u64) -> ProviderResult<Option<B256>> {
self.db.get::<tables::CanonicalHeaders>(number).map_err(Into::into)
self.static_file_provider.get_with_static_file_or_database(
StaticFileSegment::Headers,
number,
|static_file| static_file.block_hash(number),
|| Ok(self.db.get::<tables::CanonicalHeaders>(number)?),
)
}
fn canonical_hashes_range(
@ -45,16 +53,23 @@ impl<'b, TX: DbTx> BlockHashReader for LatestStateProviderRef<'b, TX> {
start: BlockNumber,
end: BlockNumber,
) -> ProviderResult<Vec<B256>> {
let range = start..end;
self.db
.cursor_read::<tables::CanonicalHeaders>()
.map(|mut cursor| {
cursor
.walk_range(range)?
.map(|result| result.map(|(_, hash)| hash).map_err(Into::into))
.collect::<ProviderResult<Vec<_>>>()
})?
.map_err(Into::into)
self.static_file_provider.get_range_with_static_file_or_database(
StaticFileSegment::Headers,
start..end,
|static_file, range, _| static_file.canonical_hashes_range(range.start, range.end),
|range, _| {
self.db
.cursor_read::<tables::CanonicalHeaders>()
.map(|mut cursor| {
cursor
.walk_range(range)?
.map(|result| result.map(|(_, hash)| hash).map_err(Into::into))
.collect::<ProviderResult<Vec<_>>>()
})?
.map_err(Into::into)
},
|_| true,
)
}
}
@ -110,18 +125,20 @@ impl<'b, TX: DbTx> StateProvider for LatestStateProviderRef<'b, TX> {
pub struct LatestStateProvider<TX: DbTx> {
/// database transaction
db: TX,
/// Static File provider
static_file_provider: StaticFileProvider,
}
impl<TX: DbTx> LatestStateProvider<TX> {
/// Create new state provider
pub fn new(db: TX) -> Self {
Self { db }
pub fn new(db: TX, static_file_provider: StaticFileProvider) -> Self {
Self { db, static_file_provider }
}
/// Returns a new provider that takes the `TX` as reference
#[inline(always)]
fn as_ref(&self) -> LatestStateProviderRef<'_, TX> {
LatestStateProviderRef::new(&self.db)
LatestStateProviderRef::new(&self.db, self.static_file_provider.clone())
}
}

View File

@ -1,58 +1,81 @@
use super::LoadedJarRef;
use super::{
metrics::{StaticFileProviderMetrics, StaticFileProviderOperation},
LoadedJarRef,
};
use crate::{
to_range, BlockHashReader, BlockNumReader, HeaderProvider, ReceiptProvider,
TransactionsProvider,
};
use reth_db::{
codecs::CompactU256,
snapshot::{HeaderMask, ReceiptMask, SnapshotCursor, TransactionMask},
static_file::{HeaderMask, ReceiptMask, StaticFileCursor, TransactionMask},
};
use reth_interfaces::provider::{ProviderError, ProviderResult};
use reth_primitives::{
Address, BlockHash, BlockHashOrNumber, BlockNumber, ChainInfo, Header, Receipt, SealedHeader,
TransactionMeta, TransactionSigned, TransactionSignedNoHash, TxHash, TxNumber, B256, U256,
};
use std::ops::{Deref, RangeBounds};
use std::{
ops::{Deref, RangeBounds},
sync::Arc,
};
/// Provider over a specific `NippyJar` and range.
#[derive(Debug)]
pub struct SnapshotJarProvider<'a> {
/// Main snapshot segment
pub struct StaticFileJarProvider<'a> {
/// Main static file segment
jar: LoadedJarRef<'a>,
/// Another kind of snapshot segment to help query data from the main one.
/// Another kind of static file segment to help query data from the main one.
auxiliar_jar: Option<Box<Self>>,
metrics: Option<Arc<StaticFileProviderMetrics>>,
}
impl<'a> Deref for SnapshotJarProvider<'a> {
impl<'a> Deref for StaticFileJarProvider<'a> {
type Target = LoadedJarRef<'a>;
fn deref(&self) -> &Self::Target {
&self.jar
}
}
impl<'a> From<LoadedJarRef<'a>> for SnapshotJarProvider<'a> {
impl<'a> From<LoadedJarRef<'a>> for StaticFileJarProvider<'a> {
fn from(value: LoadedJarRef<'a>) -> Self {
SnapshotJarProvider { jar: value, auxiliar_jar: None }
StaticFileJarProvider { jar: value, auxiliar_jar: None, metrics: None }
}
}
impl<'a> SnapshotJarProvider<'a> {
impl<'a> StaticFileJarProvider<'a> {
/// Provides a cursor for more granular data access.
pub fn cursor<'b>(&'b self) -> ProviderResult<SnapshotCursor<'a>>
pub fn cursor<'b>(&'b self) -> ProviderResult<StaticFileCursor<'a>>
where
'b: 'a,
{
SnapshotCursor::new(self.value(), self.mmap_handle())
let result = StaticFileCursor::new(self.value(), self.mmap_handle())?;
if let Some(metrics) = &self.metrics {
metrics.record_segment_operation(
self.segment(),
StaticFileProviderOperation::InitCursor,
None,
);
}
Ok(result)
}
/// Adds a new auxiliar snapshot to help query data from the main one
pub fn with_auxiliar(mut self, auxiliar_jar: SnapshotJarProvider<'a>) -> Self {
/// Adds a new auxiliar static file to help query data from the main one
pub fn with_auxiliar(mut self, auxiliar_jar: StaticFileJarProvider<'a>) -> Self {
self.auxiliar_jar = Some(Box::new(auxiliar_jar));
self
}
/// Enables metrics on the provider.
pub fn with_metrics(mut self, metrics: Arc<StaticFileProviderMetrics>) -> Self {
self.metrics = Some(metrics);
self
}
}
impl<'a> HeaderProvider for SnapshotJarProvider<'a> {
impl<'a> HeaderProvider for StaticFileJarProvider<'a> {
fn header(&self, block_hash: &BlockHash) -> ProviderResult<Option<Header>> {
Ok(self
.cursor()?
@ -124,7 +147,7 @@ impl<'a> HeaderProvider for SnapshotJarProvider<'a> {
}
}
impl<'a> BlockHashReader for SnapshotJarProvider<'a> {
impl<'a> BlockHashReader for StaticFileJarProvider<'a> {
fn block_hash(&self, number: u64) -> ProviderResult<Option<B256>> {
self.cursor()?.get_one::<HeaderMask<BlockHash>>(number.into())
}
@ -146,7 +169,7 @@ impl<'a> BlockHashReader for SnapshotJarProvider<'a> {
}
}
impl<'a> BlockNumReader for SnapshotJarProvider<'a> {
impl<'a> BlockNumReader for StaticFileJarProvider<'a> {
fn chain_info(&self) -> ProviderResult<ChainInfo> {
// Information on live database
Err(ProviderError::UnsupportedProvider)
@ -167,17 +190,17 @@ impl<'a> BlockNumReader for SnapshotJarProvider<'a> {
Ok(cursor
.get_one::<HeaderMask<BlockHash>>((&hash).into())?
.and_then(|res| (res == hash).then(|| cursor.number())))
.and_then(|res| (res == hash).then(|| cursor.number()).flatten()))
}
}
impl<'a> TransactionsProvider for SnapshotJarProvider<'a> {
impl<'a> TransactionsProvider for StaticFileJarProvider<'a> {
fn transaction_id(&self, hash: TxHash) -> ProviderResult<Option<TxNumber>> {
let mut cursor = self.cursor()?;
Ok(cursor
.get_one::<TransactionMask<TransactionSignedNoHash>>((&hash).into())?
.and_then(|res| (res.hash() == hash).then(|| cursor.number())))
.and_then(|res| (res.hash() == hash).then(|| cursor.number()).flatten()))
}
fn transaction_by_id(&self, num: TxNumber) -> ProviderResult<Option<TransactionSigned>> {
@ -205,12 +228,12 @@ impl<'a> TransactionsProvider for SnapshotJarProvider<'a> {
&self,
_hash: TxHash,
) -> ProviderResult<Option<(TransactionSigned, TransactionMeta)>> {
// Information required on indexing table [`tables::TransactionBlock`]
// Information required on indexing table [`tables::TransactionBlocks`]
Err(ProviderError::UnsupportedProvider)
}
fn transaction_block(&self, _id: TxNumber) -> ProviderResult<Option<BlockNumber>> {
// Information on indexing table [`tables::TransactionBlock`]
// Information on indexing table [`tables::TransactionBlocks`]
Err(ProviderError::UnsupportedProvider)
}
@ -218,7 +241,7 @@ impl<'a> TransactionsProvider for SnapshotJarProvider<'a> {
&self,
_block_id: BlockHashOrNumber,
) -> ProviderResult<Option<Vec<TransactionSigned>>> {
// Related to indexing tables. Live database should get the tx_range and call snapshot
// Related to indexing tables. Live database should get the tx_range and call static file
// provider with `transactions_by_tx_range` instead.
Err(ProviderError::UnsupportedProvider)
}
@ -227,7 +250,7 @@ impl<'a> TransactionsProvider for SnapshotJarProvider<'a> {
&self,
_range: impl RangeBounds<BlockNumber>,
) -> ProviderResult<Vec<Vec<TransactionSigned>>> {
// Related to indexing tables. Live database should get the tx_range and call snapshot
// Related to indexing tables. Live database should get the tx_range and call static file
// provider with `transactions_by_tx_range` instead.
Err(ProviderError::UnsupportedProvider)
}
@ -267,14 +290,14 @@ impl<'a> TransactionsProvider for SnapshotJarProvider<'a> {
}
}
impl<'a> ReceiptProvider for SnapshotJarProvider<'a> {
impl<'a> ReceiptProvider for StaticFileJarProvider<'a> {
fn receipt(&self, num: TxNumber) -> ProviderResult<Option<Receipt>> {
self.cursor()?.get_one::<ReceiptMask<Receipt>>(num.into())
}
fn receipt_by_hash(&self, hash: TxHash) -> ProviderResult<Option<Receipt>> {
if let Some(tx_snapshot) = &self.auxiliar_jar {
if let Some(num) = tx_snapshot.transaction_id(hash)? {
if let Some(tx_static_file) = &self.auxiliar_jar {
if let Some(num) = tx_static_file.transaction_id(hash)? {
return self.receipt(num)
}
}
@ -282,7 +305,7 @@ impl<'a> ReceiptProvider for SnapshotJarProvider<'a> {
}
fn receipts_by_block(&self, _block: BlockHashOrNumber) -> ProviderResult<Option<Vec<Receipt>>> {
// Related to indexing tables. Snapshot should get the tx_range and call snapshot
// Related to indexing tables. StaticFile should get the tx_range and call static file
// provider with `receipt()` instead for each
Err(ProviderError::UnsupportedProvider)
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,90 @@
use std::{collections::HashMap, time::Duration};
use itertools::Itertools;
use metrics::{Counter, Histogram};
use reth_metrics::Metrics;
use reth_primitives::StaticFileSegment;
use strum::{EnumIter, IntoEnumIterator};
/// Metrics for the static file provider.
#[derive(Debug)]
pub struct StaticFileProviderMetrics {
segment_operations: HashMap<
(StaticFileSegment, StaticFileProviderOperation),
StaticFileProviderOperationMetrics,
>,
}
impl Default for StaticFileProviderMetrics {
fn default() -> Self {
Self {
segment_operations: StaticFileSegment::iter()
.cartesian_product(StaticFileProviderOperation::iter())
.map(|(segment, operation)| {
(
(segment, operation),
StaticFileProviderOperationMetrics::new_with_labels(&[
("segment", segment.as_str()),
("operation", operation.as_str()),
]),
)
})
.collect(),
}
}
}
impl StaticFileProviderMetrics {
pub(crate) fn record_segment_operation(
&self,
segment: StaticFileSegment,
operation: StaticFileProviderOperation,
duration: Option<Duration>,
) {
self.segment_operations
.get(&(segment, operation))
.expect("segment operation metrics should exist")
.calls_total
.increment(1);
if let Some(duration) = duration {
self.segment_operations
.get(&(segment, operation))
.expect("segment operation metrics should exist")
.write_duration_seconds
.record(duration.as_secs_f64());
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter)]
pub(crate) enum StaticFileProviderOperation {
InitCursor,
OpenWriter,
Append,
Prune,
IncrementBlock,
CommitWriter,
}
impl StaticFileProviderOperation {
const fn as_str(&self) -> &'static str {
match self {
Self::InitCursor => "init-cursor",
Self::OpenWriter => "open-writer",
Self::Append => "append",
Self::Prune => "prune",
Self::IncrementBlock => "increment-block",
Self::CommitWriter => "commit-writer",
}
}
}
#[derive(Metrics)]
#[metrics(scope = "static_files.jar_provider")]
pub(crate) struct StaticFileProviderOperationMetrics {
/// Total number of static file jar provider operations made.
calls_total: Counter,
/// The time it took to execute the static file jar provider operation that writes data.
write_duration_seconds: Histogram,
}

View File

@ -1,18 +1,25 @@
mod manager;
pub use manager::SnapshotProvider;
pub use manager::{StaticFileProvider, StaticFileWriter};
mod jar;
pub use jar::SnapshotJarProvider;
pub use jar::StaticFileJarProvider;
mod writer;
pub use writer::{StaticFileProviderRW, StaticFileProviderRWRefMut};
mod metrics;
use reth_interfaces::provider::ProviderResult;
use reth_nippy_jar::NippyJar;
use reth_primitives::{snapshot::SegmentHeader, SnapshotSegment};
use reth_primitives::{static_file::SegmentHeader, StaticFileSegment};
use std::{ops::Deref, sync::Arc};
/// Alias type for each specific `NippyJar`.
type LoadedJarRef<'a> = dashmap::mapref::one::Ref<'a, (u64, SnapshotSegment), LoadedJar>;
const BLOCKS_PER_STATIC_FILE: u64 = 500_000;
/// Helper type to reuse an associated snapshot mmap handle on created cursors.
/// Alias type for each specific `NippyJar`.
type LoadedJarRef<'a> = dashmap::mapref::one::Ref<'a, (u64, StaticFileSegment), LoadedJar>;
/// Helper type to reuse an associated static file mmap handle on created cursors.
#[derive(Debug)]
pub struct LoadedJar {
jar: NippyJar<SegmentHeader>,
@ -29,6 +36,10 @@ impl LoadedJar {
fn mmap_handle(&self) -> Arc<reth_nippy_jar::DataReader> {
self.mmap_handle.clone()
}
fn segment(&self) -> StaticFileSegment {
self.jar.user_header().segment()
}
}
impl Deref for LoadedJar {
@ -45,25 +56,31 @@ mod tests {
use rand::seq::SliceRandom;
use reth_db::{
cursor::DbCursorRO,
snapshot::create_snapshot_T1_T2_T3,
static_file::create_static_file_T1_T2_T3,
transaction::{DbTx, DbTxMut},
CanonicalHeaders, HeaderNumbers, HeaderTD, Headers, RawTable,
CanonicalHeaders, HeaderNumbers, HeaderTerminalDifficulties, Headers, RawTable,
};
use reth_interfaces::test_utils::generators::{self, random_header_range};
use reth_primitives::{BlockNumber, B256, U256};
use reth_primitives::{static_file::find_fixed_range, BlockNumber, B256, U256};
#[test]
fn test_snap() {
// Ranges
let row_count = 100u64;
let range = 0..=(row_count - 1);
let segment_header =
SegmentHeader::new(range.clone(), range.clone(), SnapshotSegment::Headers);
let segment_header = SegmentHeader::new(
range.clone().into(),
Some(range.clone().into()),
Some(range.clone().into()),
StaticFileSegment::Headers,
);
// Data sources
let factory = create_test_provider_factory();
let snap_path = tempfile::tempdir().unwrap();
let snap_file = snap_path.path().join(SnapshotSegment::Headers.filename(&range, &range));
let static_files_path = tempfile::tempdir().unwrap();
let static_file = static_files_path
.path()
.join(StaticFileSegment::Headers.filename(&find_fixed_range(*range.end())));
// Setup data
let mut headers = random_header_range(
@ -81,17 +98,17 @@ mod tests {
tx.put::<CanonicalHeaders>(header.number, hash).unwrap();
tx.put::<Headers>(header.number, header.clone().unseal()).unwrap();
tx.put::<HeaderTD>(header.number, td.into()).unwrap();
tx.put::<HeaderTerminalDifficulties>(header.number, td.into()).unwrap();
tx.put::<HeaderNumbers>(hash, header.number).unwrap();
}
provider_rw.commit().unwrap();
// Create Snapshot
// Create StaticFile
{
let with_compression = true;
let with_filter = true;
let mut nippy_jar = NippyJar::new(3, snap_file.as_path(), segment_header);
let mut nippy_jar = NippyJar::new(3, static_file.as_path(), segment_header);
if with_compression {
nippy_jar = nippy_jar.with_zstd(false, 0);
@ -115,24 +132,22 @@ mod tests {
.unwrap()
.map(|row| row.map(|(_key, value)| value.into_value()).map_err(|e| e.into()));
create_snapshot_T1_T2_T3::<
create_static_file_T1_T2_T3::<
Headers,
HeaderTD,
HeaderTerminalDifficulties,
CanonicalHeaders,
BlockNumber,
SegmentHeader,
>(
tx, range, None, none_vec, Some(hashes), row_count as usize, &mut nippy_jar
)
>(tx, range, None, none_vec, Some(hashes), row_count as usize, nippy_jar)
.unwrap();
}
// Use providers to query Header data and compare if it matches
{
let db_provider = factory.provider().unwrap();
let manager = SnapshotProvider::new(snap_path.path()).unwrap().with_filters();
let manager = StaticFileProvider::new(static_files_path.path()).unwrap().with_filters();
let jar_provider = manager
.get_segment_provider_from_block(SnapshotSegment::Headers, 0, Some(&snap_file))
.get_segment_provider_from_block(StaticFileSegment::Headers, 0, Some(&static_file))
.unwrap();
assert!(!headers.is_empty());
@ -148,7 +163,7 @@ mod tests {
assert_eq!(header, db_provider.header(&header_hash).unwrap().unwrap());
assert_eq!(header, jar_provider.header(&header_hash).unwrap().unwrap());
// Compare HeaderTD
// Compare HeaderTerminalDifficulties
assert_eq!(
db_provider.header_td(&header_hash).unwrap().unwrap(),
jar_provider.header_td(&header_hash).unwrap().unwrap()

View File

@ -0,0 +1,488 @@
use crate::providers::static_file::metrics::StaticFileProviderOperation;
use super::{
manager::StaticFileProviderInner, metrics::StaticFileProviderMetrics, StaticFileProvider,
};
use dashmap::mapref::one::RefMut;
use reth_codecs::Compact;
use reth_db::codecs::CompactU256;
use reth_interfaces::provider::{ProviderError, ProviderResult};
use reth_nippy_jar::{NippyJar, NippyJarError, NippyJarWriter};
use reth_primitives::{
static_file::{find_fixed_range, SegmentHeader, SegmentRangeInclusive},
BlockHash, BlockNumber, Header, Receipt, StaticFileSegment, TransactionSignedNoHash, TxNumber,
U256,
};
use std::{
path::{Path, PathBuf},
sync::{Arc, Weak},
time::Instant,
};
use tracing::debug;
/// Mutable reference to a dashmap element of [`StaticFileProviderRW`].
pub type StaticFileProviderRWRefMut<'a> = RefMut<'a, StaticFileSegment, StaticFileProviderRW>;
#[derive(Debug)]
/// Extends `StaticFileProvider` with writing capabilities
pub struct StaticFileProviderRW {
/// Reference back to the provider. We need [Weak] here because [StaticFileProviderRW] is
/// stored in a [dashmap::DashMap] inside the parent [StaticFileProvider].which is an [Arc].
/// If we were to use an [Arc] here, we would create a reference cycle.
reader: Weak<StaticFileProviderInner>,
writer: NippyJarWriter<SegmentHeader>,
data_path: PathBuf,
buf: Vec<u8>,
metrics: Option<Arc<StaticFileProviderMetrics>>,
}
impl StaticFileProviderRW {
/// Creates a new [`StaticFileProviderRW`] for a [`StaticFileSegment`].
pub fn new(
segment: StaticFileSegment,
block: BlockNumber,
reader: Weak<StaticFileProviderInner>,
metrics: Option<Arc<StaticFileProviderMetrics>>,
) -> ProviderResult<Self> {
let (writer, data_path) = Self::open(segment, block, reader.clone(), metrics.clone())?;
Ok(Self { writer, data_path, buf: Vec::with_capacity(100), reader, metrics })
}
fn open(
segment: StaticFileSegment,
block: u64,
reader: Weak<StaticFileProviderInner>,
metrics: Option<Arc<StaticFileProviderMetrics>>,
) -> ProviderResult<(NippyJarWriter<SegmentHeader>, PathBuf)> {
let start = Instant::now();
let static_file_provider = Self::upgrade_provider_to_strong_reference(&reader);
let block_range = find_fixed_range(block);
let (jar, path) = match static_file_provider.get_segment_provider_from_block(
segment,
block_range.start(),
None,
) {
Ok(provider) => (NippyJar::load(provider.data_path())?, provider.data_path().into()),
Err(ProviderError::MissingStaticFileBlock(_, _)) => {
let path = static_file_provider.directory().join(segment.filename(&block_range));
(create_jar(segment, &path, block_range), path)
}
Err(err) => return Err(err),
};
let result = match NippyJarWriter::new(jar) {
Ok(writer) => Ok((writer, path)),
Err(NippyJarError::FrozenJar) => {
// This static file has been frozen, so we should
Err(ProviderError::FinalizedStaticFile(segment, block))
}
Err(e) => Err(e.into()),
}?;
if let Some(metrics) = &metrics {
metrics.record_segment_operation(
segment,
StaticFileProviderOperation::OpenWriter,
Some(start.elapsed()),
);
}
Ok(result)
}
/// Commits configuration changes to disk and updates the reader index with the new changes.
pub fn commit(&mut self) -> ProviderResult<()> {
let start = Instant::now();
// Commits offsets and new user_header to disk
self.writer.commit()?;
if let Some(metrics) = &self.metrics {
metrics.record_segment_operation(
self.writer.user_header().segment(),
StaticFileProviderOperation::CommitWriter,
Some(start.elapsed()),
);
}
debug!(
target: "provider::static_file",
segment = ?self.writer.user_header().segment(),
path = ?self.data_path,
duration = ?start.elapsed(),
"Commit"
);
self.update_index()?;
Ok(())
}
/// Updates the `self.reader` internal index.
fn update_index(&self) -> ProviderResult<()> {
// We find the maximum block of the segment by checking this writer's last block.
//
// However if there's no block range (because there's no data), we try to calculate it by
// substracting 1 from the expected block start, resulting on the last block of the
// previous file.
//
// If that expected block start is 0, then it means that there's no actual block data, and
// there's no block data in static files.
let segment_max_block = match self.writer.user_header().block_range() {
Some(block_range) => Some(block_range.end()),
None => {
if self.writer.user_header().expected_block_start() > 0 {
Some(self.writer.user_header().expected_block_start() - 1)
} else {
None
}
}
};
self.reader().update_index(self.writer.user_header().segment(), segment_max_block)
}
/// Allows to increment the [`SegmentHeader`] end block. It will commit the current static file,
/// and create the next one if we are past the end range.
///
/// Returns the current [`BlockNumber`] as seen in the static file.
pub fn increment_block(&mut self, segment: StaticFileSegment) -> ProviderResult<BlockNumber> {
let start = Instant::now();
if let Some(last_block) = self.writer.user_header().block_end() {
// We have finished the previous static file and must freeze it
if last_block == self.writer.user_header().expected_block_end() {
// Commits offsets and new user_header to disk
self.commit()?;
// Opens the new static file
let (writer, data_path) =
Self::open(segment, last_block + 1, self.reader.clone(), self.metrics.clone())?;
self.writer = writer;
self.data_path = data_path;
*self.writer.user_header_mut() =
SegmentHeader::new(find_fixed_range(last_block + 1), None, None, segment);
}
}
let block = self.writer.user_header_mut().increment_block();
if let Some(metrics) = &self.metrics {
metrics.record_segment_operation(
segment,
StaticFileProviderOperation::IncrementBlock,
Some(start.elapsed()),
);
}
Ok(block)
}
/// Truncates a number of rows from disk. It deletes and loads an older static file if block
/// goes beyond the start of the current block range.
///
/// **last_block** should be passed only with transaction based segments.
///
/// # Note
/// Commits to the configuration file at the end.
fn truncate(
&mut self,
segment: StaticFileSegment,
mut num_rows: u64,
last_block: Option<u64>,
) -> ProviderResult<()> {
while num_rows > 0 {
let len = match segment {
StaticFileSegment::Headers => {
self.writer.user_header().block_len().unwrap_or_default()
}
StaticFileSegment::Transactions | StaticFileSegment::Receipts => {
self.writer.user_header().tx_len().unwrap_or_default()
}
};
if num_rows >= len {
// If there's more rows to delete than this static file contains, then just
// delete the whole file and go to the next static file
let previous_snap = self.data_path.clone();
let block_start = self.writer.user_header().expected_block_start();
if block_start != 0 {
let (writer, data_path) = Self::open(
segment,
self.writer.user_header().expected_block_start() - 1,
self.reader.clone(),
self.metrics.clone(),
)?;
self.writer = writer;
self.data_path = data_path;
NippyJar::<SegmentHeader>::load(&previous_snap)?.delete()?;
} else {
// Update `SegmentHeader`
self.writer.user_header_mut().prune(len);
self.writer.prune_rows(len as usize)?;
break
}
num_rows -= len;
} else {
// Update `SegmentHeader`
self.writer.user_header_mut().prune(num_rows);
// Truncate data
self.writer.prune_rows(num_rows as usize)?;
num_rows = 0;
}
}
// Only Transactions and Receipts
if let Some(last_block) = last_block {
let header = self.writer.user_header_mut();
header.set_block_range(header.expected_block_start(), last_block);
}
// Commits new changes to disk.
self.commit()?;
Ok(())
}
/// Appends column to static file.
fn append_column<T: Compact>(&mut self, column: T) -> ProviderResult<()> {
self.buf.clear();
column.to_compact(&mut self.buf);
self.writer.append_column(Some(Ok(&self.buf)))?;
Ok(())
}
/// Appends to tx number-based static file.
///
/// Returns the current [`TxNumber`] as seen in the static file.
fn append_with_tx_number<V: Compact>(
&mut self,
segment: StaticFileSegment,
tx_num: TxNumber,
value: V,
) -> ProviderResult<TxNumber> {
debug_assert!(self.writer.user_header().segment() == segment);
if self.writer.user_header().tx_range().is_none() {
self.writer.user_header_mut().set_tx_range(tx_num, tx_num);
} else {
self.writer.user_header_mut().increment_tx();
}
self.append_column(value)?;
Ok(self.writer.user_header().tx_end().expect("qed"))
}
/// Appends header to static file.
///
/// It **CALLS** `increment_block()` since the number of headers is equal to the number of
/// blocks.
///
/// Returns the current [`BlockNumber`] as seen in the static file.
pub fn append_header(
&mut self,
header: Header,
terminal_difficulty: U256,
hash: BlockHash,
) -> ProviderResult<BlockNumber> {
let start = Instant::now();
debug_assert!(self.writer.user_header().segment() == StaticFileSegment::Headers);
let block_number = self.increment_block(StaticFileSegment::Headers)?;
self.append_column(header)?;
self.append_column(CompactU256::from(terminal_difficulty))?;
self.append_column(hash)?;
if let Some(metrics) = &self.metrics {
metrics.record_segment_operation(
StaticFileSegment::Headers,
StaticFileProviderOperation::Append,
Some(start.elapsed()),
);
}
Ok(block_number)
}
/// Appends transaction to static file.
///
/// It **DOES NOT CALL** `increment_block()`, it should be handled elsewhere. There might be
/// empty blocks and this function wouldn't be called.
///
/// Returns the current [`TxNumber`] as seen in the static file.
pub fn append_transaction(
&mut self,
tx_num: TxNumber,
tx: TransactionSignedNoHash,
) -> ProviderResult<TxNumber> {
let start = Instant::now();
let result = self.append_with_tx_number(StaticFileSegment::Transactions, tx_num, tx)?;
if let Some(metrics) = &self.metrics {
metrics.record_segment_operation(
StaticFileSegment::Transactions,
StaticFileProviderOperation::Append,
Some(start.elapsed()),
);
}
Ok(result)
}
/// Appends receipt to static file.
///
/// It **DOES NOT** call `increment_block()`, it should be handled elsewhere. There might be
/// empty blocks and this function wouldn't be called.
///
/// Returns the current [`TxNumber`] as seen in the static file.
pub fn append_receipt(
&mut self,
tx_num: TxNumber,
receipt: Receipt,
) -> ProviderResult<TxNumber> {
let start = Instant::now();
let result = self.append_with_tx_number(StaticFileSegment::Receipts, tx_num, receipt)?;
if let Some(metrics) = &self.metrics {
metrics.record_segment_operation(
StaticFileSegment::Receipts,
StaticFileProviderOperation::Append,
Some(start.elapsed()),
);
}
Ok(result)
}
/// Removes the last `number` of transactions from static files.
///
/// # Note
/// Commits to the configuration file at the end.
pub fn prune_transactions(
&mut self,
number: u64,
last_block: BlockNumber,
) -> ProviderResult<()> {
let start = Instant::now();
let segment = StaticFileSegment::Transactions;
debug_assert!(self.writer.user_header().segment() == segment);
self.truncate(segment, number, Some(last_block))?;
if let Some(metrics) = &self.metrics {
metrics.record_segment_operation(
StaticFileSegment::Transactions,
StaticFileProviderOperation::Prune,
Some(start.elapsed()),
);
}
Ok(())
}
/// Prunes `to_delete` number of receipts from static_files.
///
/// # Note
/// Commits to the configuration file at the end.
pub fn prune_receipts(
&mut self,
to_delete: u64,
last_block: BlockNumber,
) -> ProviderResult<()> {
let start = Instant::now();
let segment = StaticFileSegment::Receipts;
debug_assert!(self.writer.user_header().segment() == segment);
self.truncate(segment, to_delete, Some(last_block))?;
if let Some(metrics) = &self.metrics {
metrics.record_segment_operation(
StaticFileSegment::Receipts,
StaticFileProviderOperation::Prune,
Some(start.elapsed()),
);
}
Ok(())
}
/// Prunes `to_delete` number of headers from static_files.
///
/// # Note
/// Commits to the configuration file at the end.
pub fn prune_headers(&mut self, to_delete: u64) -> ProviderResult<()> {
let start = Instant::now();
let segment = StaticFileSegment::Headers;
debug_assert!(self.writer.user_header().segment() == segment);
self.truncate(segment, to_delete, None)?;
if let Some(metrics) = &self.metrics {
metrics.record_segment_operation(
StaticFileSegment::Headers,
StaticFileProviderOperation::Prune,
Some(start.elapsed()),
);
}
Ok(())
}
fn reader(&self) -> StaticFileProvider {
Self::upgrade_provider_to_strong_reference(&self.reader)
}
/// Upgrades a weak reference of [`StaticFileProviderInner`] to a strong reference
/// [`StaticFileProvider`].
///
/// # Panics
///
/// Panics if the parent [`StaticFileProvider`] is fully dropped while the child writer is still
/// active. In reality, it's impossible to detach the [`StaticFileProviderRW`] from the
/// [`StaticFileProvider`].
fn upgrade_provider_to_strong_reference(
provider: &Weak<StaticFileProviderInner>,
) -> StaticFileProvider {
provider.upgrade().map(StaticFileProvider).expect("StaticFileProvider is dropped")
}
#[cfg(any(test, feature = "test-utils"))]
/// Helper function to override block range for testing.
pub fn set_block_range(&mut self, block_range: std::ops::RangeInclusive<BlockNumber>) {
self.writer.user_header_mut().set_block_range(*block_range.start(), *block_range.end())
}
}
fn create_jar(
segment: StaticFileSegment,
path: &Path,
expected_block_range: SegmentRangeInclusive,
) -> NippyJar<SegmentHeader> {
let mut jar = NippyJar::new(
segment.columns(),
path,
SegmentHeader::new(expected_block_range, None, None, segment),
);
// Transaction and Receipt already have the compression scheme used natively in its encoding.
// (zstd-dictionary)
if segment.is_headers() {
jar = jar.with_lz4();
}
jar
}

View File

@ -26,7 +26,10 @@ pub fn assert_genesis_block<DB: Database>(provider: &DatabaseProviderRW<DB>, g:
assert_eq!(tx.table::<tables::HeaderNumbers>().unwrap(), vec![(h, n)]);
assert_eq!(tx.table::<tables::CanonicalHeaders>().unwrap(), vec![(n, h)]);
assert_eq!(tx.table::<tables::HeaderTD>().unwrap(), vec![(n, g.difficulty.into())]);
assert_eq!(
tx.table::<tables::HeaderTerminalDifficulties>().unwrap(),
vec![(n, g.difficulty.into())]
);
assert_eq!(
tx.table::<tables::BlockBodyIndices>().unwrap(),
vec![(0, StoredBlockBodyIndices::default())]
@ -34,23 +37,23 @@ pub fn assert_genesis_block<DB: Database>(provider: &DatabaseProviderRW<DB>, g:
assert_eq!(tx.table::<tables::BlockOmmers>().unwrap(), vec![]);
assert_eq!(tx.table::<tables::BlockWithdrawals>().unwrap(), vec![]);
assert_eq!(tx.table::<tables::Transactions>().unwrap(), vec![]);
assert_eq!(tx.table::<tables::TransactionBlock>().unwrap(), vec![]);
assert_eq!(tx.table::<tables::TxHashNumber>().unwrap(), vec![]);
assert_eq!(tx.table::<tables::TransactionBlocks>().unwrap(), vec![]);
assert_eq!(tx.table::<tables::TransactionHashNumbers>().unwrap(), vec![]);
assert_eq!(tx.table::<tables::Receipts>().unwrap(), vec![]);
assert_eq!(tx.table::<tables::PlainAccountState>().unwrap(), vec![]);
assert_eq!(tx.table::<tables::PlainStorageState>().unwrap(), vec![]);
assert_eq!(tx.table::<tables::AccountHistory>().unwrap(), vec![]);
assert_eq!(tx.table::<tables::StorageHistory>().unwrap(), vec![]);
assert_eq!(tx.table::<tables::AccountsHistory>().unwrap(), vec![]);
assert_eq!(tx.table::<tables::StoragesHistory>().unwrap(), vec![]);
// TODO check after this gets done: https://github.com/paradigmxyz/reth/issues/1588
// Bytecodes are not reverted assert_eq!(tx.table::<tables::Bytecodes>().unwrap(), vec![]);
assert_eq!(tx.table::<tables::AccountChangeSet>().unwrap(), vec![]);
assert_eq!(tx.table::<tables::StorageChangeSet>().unwrap(), vec![]);
assert_eq!(tx.table::<tables::HashedAccount>().unwrap(), vec![]);
assert_eq!(tx.table::<tables::HashedStorage>().unwrap(), vec![]);
assert_eq!(tx.table::<tables::AccountChangeSets>().unwrap(), vec![]);
assert_eq!(tx.table::<tables::StorageChangeSets>().unwrap(), vec![]);
assert_eq!(tx.table::<tables::HashedAccounts>().unwrap(), vec![]);
assert_eq!(tx.table::<tables::HashedStorages>().unwrap(), vec![]);
assert_eq!(tx.table::<tables::AccountsTrie>().unwrap(), vec![]);
assert_eq!(tx.table::<tables::StoragesTrie>().unwrap(), vec![]);
assert_eq!(tx.table::<tables::TxSenders>().unwrap(), vec![]);
// SyncStage is not updated in tests
assert_eq!(tx.table::<tables::TransactionSenders>().unwrap(), vec![]);
// StageCheckpoints is not updated in tests
}
const BLOCK_RLP: [u8; 610] = hex!("f9025ff901f7a0c86e8cc0310ae7c531c758678ddbfd16fc51c8cef8cec650b032de9869e8b94fa01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347942adc25665018aa1fe0e6bc666dac8fc2697ff9baa050554882fbbda2c2fd93fdc466db9946ea262a67f7a76cc169e714f105ab583da00967f09ef1dfed20c0eacfaa94d5cd4002eda3242ac47eae68972d07b106d192a0e3c8b47fbfc94667ef4cceb17e5cc21e3b1eebd442cebb27f07562b33836290db90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008302000001830f42408238108203e800a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f862f860800a83061a8094095e7baea6a6c7c4c2dfeb977efac326af552d8780801ba072ed817487b84ba367d15d2f039b5fc5f087d0a8882fbdf73e8cb49357e1ce30a0403d800545b8fc544f92ce8124e2255f8c3c6af93f28243a120585d4c4c6a2a3c0");

View File

@ -1,6 +1,6 @@
use crate::ProviderFactory;
use reth_db::{
test_utils::{create_test_rw_db, TempDatabase},
test_utils::{create_test_rw_db, create_test_static_files_dir, TempDatabase},
DatabaseEnv,
};
use reth_primitives::{ChainSpec, MAINNET};
@ -27,5 +27,6 @@ pub fn create_test_provider_factory_with_chain_spec(
chain_spec: Arc<ChainSpec>,
) -> ProviderFactory<Arc<TempDatabase<DatabaseEnv>>> {
let db = create_test_rw_db();
ProviderFactory::new(db, chain_spec)
ProviderFactory::new(db, chain_spec, create_test_static_files_dir())
.expect("create provider factory with static_files")
}

View File

@ -20,7 +20,7 @@ pub trait HashingWriter: Send + Sync {
range: RangeInclusive<BlockNumber>,
) -> ProviderResult<BTreeMap<B256, Option<Account>>>;
/// Inserts all accounts into [reth_db::tables::AccountHistory] table.
/// Inserts all accounts into [reth_db::tables::AccountsHistory] table.
///
/// # Returns
///

View File

@ -71,3 +71,6 @@ pub use prune_checkpoint::{PruneCheckpointReader, PruneCheckpointWriter};
mod database_provider;
pub use database_provider::DatabaseProviderFactory;
mod stats;
pub use stats::StatsReader;

View File

@ -0,0 +1,10 @@
use reth_db::table::Table;
use reth_interfaces::provider::ProviderResult;
/// The trait for fetching provider statistics.
#[auto_impl::auto_impl(&, Arc)]
pub trait StatsReader: Send + Sync {
/// Fetch the number of entries in the corresponding [Table]. Depending on the provider, it may
/// route to different data sources other than [Table].
fn count_entries<T: Table>(&self) -> ProviderResult<usize>;
}