diff --git a/CHANGELOG.md b/CHANGELOG.md index 46f21c6cda37..ab3de4364636 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,8 @@ - [#7096](https://github.com/ChainSafe/forest/issues/7096): `eth_subscribe` `logs` now re-emits the logs of reorg-reverted tipsets with `removed: true`, ahead of the logs of the replacing tipsets. +- [#7156](https://github.com/ChainSafe/forest/pull/7156): The `eth` block `logsBloom` is now correctly computed from the block's logs instead of being all-ones. When the chain indexer is enabled, the computed bloom is cached in the database. + ## Forest v0.33.7 "Shimmergloom" ### Added diff --git a/Cargo.lock b/Cargo.lock index 948cd71d316a..841829c345e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3243,7 +3243,7 @@ checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] name = "forest-filecoin" -version = "0.33.7" +version = "0.33.8" dependencies = [ "ahash", "all_asserts", diff --git a/Cargo.toml b/Cargo.toml index ddae566ea4bb..411feb006402 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "forest-filecoin" -version = "0.33.7" +version = "0.33.8" authors = ["ChainSafe Systems "] repository = "https://github.com/ChainSafe/forest" edition = "2024" diff --git a/scripts/tests/api_compare/filter-list b/scripts/tests/api_compare/filter-list index c2c32dc64f46..0656b736d920 100644 --- a/scripts/tests/api_compare/filter-list +++ b/scripts/tests/api_compare/filter-list @@ -1,5 +1,2 @@ # This list contains potentially broken methods (or tests) that are ignored. # They should be considered bugged, and not used until the root cause is resolved. - -!Filecoin.EthGetBlockByHash -!Filecoin.EthGetBlockByNumber diff --git a/scripts/tests/api_compare/filter-list-gateway b/scripts/tests/api_compare/filter-list-gateway index c32db2e8bc4c..abce86c8f40c 100644 --- a/scripts/tests/api_compare/filter-list-gateway +++ b/scripts/tests/api_compare/filter-list-gateway @@ -64,6 +64,3 @@ # broken # https://github.com/filecoin-project/lotus/pull/13562 !Filecoin.EthGetTransactionReceiptLimited -# https://github.com/filecoin-project/lotus/pull/13618 -!Filecoin.EthGetBlockByHash -!Filecoin.EthGetBlockByNumber diff --git a/src/db/car/many.rs b/src/db/car/many.rs index adbeaf3e5fe1..e1d75caef045 100644 --- a/src/db/car/many.rs +++ b/src/db/car/many.rs @@ -12,8 +12,8 @@ use super::{AnyCar, ZstdFrameCache}; use crate::blocks::TipsetKey; use crate::db::parity_db::GarbageCollectableDb; use crate::db::{ - BlockstoreWriteOpsSubscribable, EthMappingsStore, MemoryDB, PersistentStore, SettingsStore, - SettingsStoreExt, + BLOCK_BLOOM_LEN, BlockstoreWriteOpsSubscribable, EthBlockBloomStore, EthMappingsStore, + MemoryDB, PersistentStore, SettingsStore, SettingsStoreExt, }; use crate::libp2p_bitswap::BitswapStoreReadWrite; use crate::prelude::*; @@ -315,6 +315,25 @@ impl EthMappingsStore for ManyCar { } } +impl EthBlockBloomStore for ManyCar { + fn read_bloom(&self, key: &Cid) -> anyhow::Result> { + EthBlockBloomStore::read_bloom(self.writer(), key) + } + + fn write_bloom( + &self, + key: &Cid, + height: ChainEpoch, + bloom: &[u8; BLOCK_BLOOM_LEN], + ) -> anyhow::Result<()> { + EthBlockBloomStore::write_bloom(self.writer(), key, height, bloom) + } + + fn delete_blooms_before_height(&self, height: ChainEpoch) -> anyhow::Result<()> { + EthBlockBloomStore::delete_blooms_before_height(self.writer(), height) + } +} + impl super::super::HeaviestTipsetKeyProvider for ManyCar { fn heaviest_tipset_key(&self) -> anyhow::Result> { match SettingsStoreExt::read_obj::(self, crate::db::setting_keys::HEAD_KEY)? { diff --git a/src/db/db_impl.rs b/src/db/db_impl.rs index 2936e195c7f6..a2ad86c8eec6 100644 --- a/src/db/db_impl.rs +++ b/src/db/db_impl.rs @@ -17,6 +17,7 @@ use std::path::PathBuf; #[derive(Delegate)] #[delegate(SettingsStore)] #[delegate(EthMappingsStore)] +#[delegate(EthBlockBloomStore)] #[delegate(HeaviestTipsetKeyProvider)] #[delegate(BitswapStoreRead)] #[delegate(BitswapStoreReadWrite)] diff --git a/src/db/gc/snapshot.rs b/src/db/gc/snapshot.rs index 911ea668cfef..897b343c28fd 100644 --- a/src/db/gc/snapshot.rs +++ b/src/db/gc/snapshot.rs @@ -43,7 +43,7 @@ use crate::cid_collections::FileBackedCidHashSet; use crate::cli_shared::chain_path; use crate::db::DbImpl; use crate::db::{ - BlockstoreWriteOpsSubscribable, CAR_DB_DIR_NAME, HeaviestTipsetKeyProvider, + BlockstoreWriteOpsSubscribable, CAR_DB_DIR_NAME, EthBlockBloomStore, HeaviestTipsetKeyProvider, car::{ForestCar, ReloadableManyCar, forest::new_forest_car_temp_path_in}, db_engine::db_root, parity_db::GarbageCollectableDb, @@ -348,6 +348,14 @@ impl SnapshotGarbageCollector { .unwrap_or_default() ); + // Prune blooms whose events are no longer retained by the lite snapshot. + if let Ok(head) = db.heaviest_car_tipset() { + let cutoff = head.epoch() - self.recent_state_roots; + if let Err(e) = db.delete_blooms_before_height(cutoff) { + tracing::warn!("failed to prune stale block blooms: {e:#}"); + } + } + // Reset chain head. Note that `self.exported_head_key` is guaranteed to be present, // see `*self.exported_head_key.write() = Some(head_ts.key().clone());` in `export_snapshot`. for tsk_opt in [ diff --git a/src/db/memory.rs b/src/db/memory.rs index c72baee39b27..a4bc83586af9 100644 --- a/src/db/memory.rs +++ b/src/db/memory.rs @@ -1,7 +1,10 @@ // Copyright 2019-2026 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT -use super::{EthMappingsStore, SettingsStore, SettingsStoreExt}; +use super::{ + BLOCK_BLOOM_LEN, EthBlockBloomStore, EthMappingsStore, SettingsStore, SettingsStoreExt, + decode_block_bloom, encode_block_bloom, +}; use crate::blocks::{Tipset, TipsetKey}; use crate::db::PersistentStore; use crate::libp2p_bitswap::{BitswapStoreRead, BitswapStoreReadWrite}; @@ -21,6 +24,7 @@ pub struct MemoryDB { settings_db: RwLock>>, pub eth_mappings_db: RwLock>>, ts_lookup_db: RwLock>, + eth_block_bloom_db: RwLock>>, } impl MemoryDB { @@ -143,6 +147,35 @@ impl EthMappingsStore for MemoryDB { } } +impl EthBlockBloomStore for MemoryDB { + fn read_bloom(&self, key: &Cid) -> anyhow::Result> { + Ok(self + .eth_block_bloom_db + .read() + .get(key) + .and_then(|entry| decode_block_bloom(entry).map(|(_, bloom)| *bloom))) + } + + fn write_bloom( + &self, + key: &Cid, + height: ChainEpoch, + bloom: &[u8; BLOCK_BLOOM_LEN], + ) -> anyhow::Result<()> { + self.eth_block_bloom_db + .write() + .insert(*key, encode_block_bloom(height, bloom)); + Ok(()) + } + + fn delete_blooms_before_height(&self, height: ChainEpoch) -> anyhow::Result<()> { + self.eth_block_bloom_db + .write() + .retain(|_, entry| decode_block_bloom(entry).is_some_and(|(h, _)| h >= height)); + Ok(()) + } +} + impl Blockstore for MemoryDB { fn get(&self, k: &Cid) -> anyhow::Result>> { Ok(self.blockchain_db.read().get(k).cloned().or(self @@ -234,4 +267,14 @@ mod tests { assert!(car.has(&key1).unwrap()); assert!(car.has(&key2).unwrap()); } + + #[test] + fn block_bloom_encode_decode() { + let bloom = [0xab; 256]; + let entry = encode_block_bloom(42, &bloom); + let (height, decoded) = decode_block_bloom(&entry).unwrap(); + assert_eq!(height, 42); + assert_eq!(decoded, &bloom); + assert!(decode_block_bloom(&[0, 1, 2]).is_none()); + } } diff --git a/src/db/migration/migration_map.rs b/src/db/migration/migration_map.rs index 7ffbaffb9a8a..4ac0029b10e1 100644 --- a/src/db/migration/migration_map.rs +++ b/src/db/migration/migration_map.rs @@ -11,6 +11,7 @@ use crate::Config; use crate::db::migration::v0_22_1::Migration0_22_0_0_22_1; use crate::db::migration::v0_26_0::Migration0_25_3_0_26_0; use crate::db::migration::v0_31_0::Migration0_30_5_0_31_0; +use crate::db::migration::v0_33_8::Migration0_33_7_0_33_8; use anyhow::Context as _; use anyhow::bail; use itertools::Itertools; @@ -157,6 +158,7 @@ create_migrations!( "0.22.0" -> "0.22.1" @ Migration0_22_0_0_22_1, "0.25.3" -> "0.26.0" @ Migration0_25_3_0_26_0, "0.30.5" -> "0.31.0" @ Migration0_30_5_0_31_0, + "0.33.7" -> "0.33.8" @ Migration0_33_7_0_33_8, ); /// Creates a migration chain from `start` to `goal`. The chain is chosen to be the shortest diff --git a/src/db/migration/mod.rs b/src/db/migration/mod.rs index 522da4b1b946..7a9abe7175f9 100644 --- a/src/db/migration/mod.rs +++ b/src/db/migration/mod.rs @@ -6,6 +6,7 @@ mod migration_map; mod v0_22_1; mod v0_26_0; mod v0_31_0; +mod v0_33_8; mod void_migration; pub use db_migration::DbMigration; diff --git a/src/db/migration/v0_33_8.rs b/src/db/migration/v0_33_8.rs new file mode 100644 index 000000000000..7a63ecf2bc19 --- /dev/null +++ b/src/db/migration/v0_33_8.rs @@ -0,0 +1,141 @@ +// Copyright 2019-2026 ChainSafe Systems +// SPDX-License-Identifier: Apache-2.0, MIT + +//! Migration logic for databases with the v0.33.7 schema to v0.33.8. +//! An `EthBlockBloom` column has been added to store per-tipset Ethereum block logs blooms. + +use super::migration_map::MigrationOperation; +use crate::Config; +use crate::db::migration::migration_map::MigrationOperationExt as _; +use anyhow::Context; +use semver::Version; +use std::path::{Path, PathBuf}; +use tracing::info; + +pub(super) struct Migration0_33_7_0_33_8 { + from: Version, + to: Version, +} + +/// Migrates the database from version 0.33.7 to 0.33.8 +impl MigrationOperation for Migration0_33_7_0_33_8 { + fn new(from: Version, to: Version) -> Self + where + Self: Sized, + { + Self { from, to } + } + + fn from(&self) -> &Version { + &self.from + } + + fn to(&self) -> &Version { + &self.to + } + + fn migrate_core(&self, chain_data_path: &Path, _: &Config) -> anyhow::Result { + let old_db = self.old_db_path(chain_data_path); + let temp_db = self.temporary_db_path(chain_data_path); + + info!( + "Renaming database directory from {} to {}", + old_db.display(), + temp_db.display() + ); + std::fs::rename(&old_db, &temp_db).context("failed to rename database directory")?; + + info!("Adding EthBlockBloom column to database"); + let mut opts = paritydb_0_33_7::to_options(temp_db.clone()); + if let Err(e) = + parity_db::Db::add_column(&mut opts, paritydb_0_33_7::eth_block_bloom_column_options()) + { + // Restore the original database so a failed migration never strands the only copy in temp. + if let Err(restore) = std::fs::rename(&temp_db, &old_db) { + tracing::error!( + "failed to restore database to {}; data is preserved at {}: {restore}", + old_db.display(), + temp_db.display() + ); + } + return Err(e).context("failed to add EthBlockBloom column"); + } + + // Create a placeholder so the delete step in `migrate` succeeds. + std::fs::create_dir_all(&old_db).context("failed to create placeholder directory")?; + + info!("Migration completed successfully"); + Ok(temp_db) + } +} + +/// Database settings from Forest `v0.33.7` +mod paritydb_0_33_7 { + use parity_db::{ColumnOptions, CompressionType, Options}; + use std::path::PathBuf; + use strum::{Display, EnumIter, IntoEnumIterator}; + + #[derive(Copy, Clone, Debug, PartialEq, EnumIter, Display)] + #[repr(u8)] + pub(super) enum DbColumn { + GraphDagCborBlake2b256, + GraphFull, + Settings, + EthMappings, + PersistentGraph, + } + + impl DbColumn { + fn create_column_options(compression: CompressionType) -> Vec { + DbColumn::iter() + .map(|col| match col { + DbColumn::GraphDagCborBlake2b256 | DbColumn::PersistentGraph => ColumnOptions { + preimage: true, + compression, + ..Default::default() + }, + DbColumn::GraphFull => ColumnOptions { + preimage: true, + btree_index: true, + compression, + ..Default::default() + }, + DbColumn::Settings => ColumnOptions { + preimage: false, + btree_index: true, + compression, + ..Default::default() + }, + DbColumn::EthMappings => ColumnOptions { + preimage: false, + btree_index: false, + compression, + ..Default::default() + }, + }) + .collect() + } + } + + /// Options for the `EthBlockBloom` column introduced in v0.33.8. + pub(super) fn eth_block_bloom_column_options() -> ColumnOptions { + ColumnOptions { + preimage: false, + btree_index: true, + compression: CompressionType::Lz4, + ..Default::default() + } + } + + pub(super) fn to_options(path: PathBuf) -> Options { + Options { + path, + sync_wal: true, + sync_data: true, + stats: false, + salt: None, + columns: DbColumn::create_column_options(CompressionType::Lz4), + compression_threshold: [(0, 128)].into_iter().collect(), + } + } +} diff --git a/src/db/mod.rs b/src/db/mod.rs index 78518517cdc6..35f1561e7c1e 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -23,6 +23,7 @@ pub use memory::MemoryDB; use crate::blocks::{Tipset, TipsetKey}; use crate::rpc::eth::types::EthHash; +use crate::shim::clock::ChainEpoch; use ambassador::delegatable_trait; use anyhow::Context as _; use cid::Cid; @@ -138,6 +139,49 @@ impl EthMappingsStoreExt for T { } } +/// Length in bytes of a stored Ethereum block logs bloom. +pub(crate) const BLOCK_BLOOM_LEN: usize = 256; + +/// Interface used to store and retrieve per-tipset Ethereum block logs blooms. +#[auto_impl::auto_impl(&, Arc)] +#[delegatable_trait] +pub trait EthBlockBloomStore { + /// Reads the logs bloom stored for the given tipset key CID. A missing or malformed entry + /// is reported as `None`. + fn read_bloom(&self, key: &Cid) -> anyhow::Result>; + + /// Stores the logs bloom for the given tipset key CID, tagged with its height. + fn write_bloom( + &self, + key: &Cid, + height: ChainEpoch, + bloom: &[u8; BLOCK_BLOOM_LEN], + ) -> anyhow::Result<()>; + + /// Deletes every stored bloom whose height is below `height`. + fn delete_blooms_before_height(&self, height: ChainEpoch) -> anyhow::Result<()>; +} + +/// Encodes a block bloom entry as its little-endian height followed by the bloom bytes. +pub(crate) fn encode_block_bloom(height: ChainEpoch, bloom: &[u8; BLOCK_BLOOM_LEN]) -> Vec { + let mut entry = Vec::with_capacity(size_of::() + BLOCK_BLOOM_LEN); + entry.extend_from_slice(&height.to_le_bytes()); + entry.extend_from_slice(bloom); + entry +} + +/// Splits a block bloom entry into its (height and `BLOCK_BLOOM_LEN`) and raw bloom bytes. +pub(crate) fn decode_block_bloom(entry: &[u8]) -> Option<(ChainEpoch, &[u8; BLOCK_BLOOM_LEN])> { + if entry.len() != size_of::() + BLOCK_BLOOM_LEN { + return None; + } + let (height, bloom) = entry.split_at(size_of::()); + Some(( + i64::from_le_bytes(height.try_into().ok()?), + bloom.try_into().ok()?, + )) +} + /// Traits for collecting DB stats pub trait DBStatistics { fn get_statistics(&self) -> Option { diff --git a/src/db/parity_db.rs b/src/db/parity_db.rs index 12d913e35063..2beade092b98 100644 --- a/src/db/parity_db.rs +++ b/src/db/parity_db.rs @@ -4,12 +4,16 @@ mod gc; pub use gc::*; -use super::{EthMappingsStore, PersistentStore, SettingsStore}; +use super::{ + BLOCK_BLOOM_LEN, EthBlockBloomStore, EthMappingsStore, PersistentStore, SettingsStore, + decode_block_bloom, encode_block_bloom, +}; use crate::blocks::{Tipset, TipsetKey}; use crate::db::{DBStatistics, parity_db_config::ParityDbConfig}; use crate::libp2p_bitswap::{BitswapStoreRead, BitswapStoreReadWrite}; use crate::prelude::*; use crate::rpc::eth::types::EthHash; +use crate::shim::clock::ChainEpoch; use crate::utils::{broadcast::has_subscribers, multihash::prelude::*}; use bytes::Bytes; use fvm_ipld_encoding::DAG_CBOR; @@ -40,6 +44,8 @@ pub enum DbColumn { /// Anything stored in this column can be considered permanent, unless manually /// deleted. PersistentGraph, + /// Column for storing per-tipset Ethereum block logs blooms. + EthBlockBloom, } impl DbColumn { @@ -76,6 +82,12 @@ impl DbColumn { compression, ..Default::default() }, + DbColumn::EthBlockBloom => parity_db::ColumnOptions { + preimage: false, + btree_index: true, + compression, + ..Default::default() + }, } }) .collect() @@ -240,6 +252,43 @@ impl EthMappingsStore for ParityDb { } } +impl EthBlockBloomStore for ParityDb { + fn read_bloom(&self, key: &Cid) -> anyhow::Result> { + Ok(self + .read_from_column(key.to_bytes(), DbColumn::EthBlockBloom)? + .and_then(|entry| decode_block_bloom(&entry).map(|(_, bloom)| *bloom))) + } + + fn write_bloom( + &self, + key: &Cid, + height: ChainEpoch, + bloom: &[u8; BLOCK_BLOOM_LEN], + ) -> anyhow::Result<()> { + self.write_to_column( + key.to_bytes(), + encode_block_bloom(height, bloom), + DbColumn::EthBlockBloom, + ) + } + + fn delete_blooms_before_height(&self, height: ChainEpoch) -> anyhow::Result<()> { + let mut stale = Vec::new(); + let mut iter = self.db.iter(DbColumn::EthBlockBloom as u8)?; + while let Some((key, entry)) = iter.next()? { + // Drop rows below the cutoff and any that fail to decode (corrupt/garbage). + if decode_block_bloom(&entry).is_none_or(|(h, _)| h < height) { + stale.push(key); + } + } + Ok(self.db.commit_changes( + stale + .into_iter() + .map(|key| (DbColumn::EthBlockBloom as u8, Operation::Dereference(key))), + )?) + } +} + impl Blockstore for ParityDb { fn get(&self, k: &Cid) -> anyhow::Result>> { let column = Self::choose_column(k); @@ -452,6 +501,7 @@ mod test { DbColumn::GraphFull => DbColumn::GraphDagCborBlake2b256, DbColumn::Settings => panic!("invalid column for IPLD data"), DbColumn::EthMappings => panic!("invalid column for IPLD data"), + DbColumn::EthBlockBloom => panic!("invalid column for IPLD data"), DbColumn::PersistentGraph => panic!("invalid column for GC enabled IPLD data"), }; let actual = db.read_from_column(cid.to_bytes(), other_column).unwrap(); diff --git a/src/db/parity_db/gc.rs b/src/db/parity_db/gc.rs index f7b49468f419..3607bb2c5ead 100644 --- a/src/db/parity_db/gc.rs +++ b/src/db/parity_db/gc.rs @@ -161,6 +161,25 @@ impl EthMappingsStore for GarbageCollectableParityDb { } } +impl EthBlockBloomStore for GarbageCollectableParityDb { + fn read_bloom(&self, key: &Cid) -> anyhow::Result> { + EthBlockBloomStore::read_bloom(&*self.db.read(), key) + } + + fn write_bloom( + &self, + key: &Cid, + height: ChainEpoch, + bloom: &[u8; BLOCK_BLOOM_LEN], + ) -> anyhow::Result<()> { + EthBlockBloomStore::write_bloom(&*self.db.read(), key, height, bloom) + } + + fn delete_blooms_before_height(&self, height: ChainEpoch) -> anyhow::Result<()> { + EthBlockBloomStore::delete_blooms_before_height(&*self.db.read(), height) + } +} + impl PersistentStore for GarbageCollectableParityDb { fn put_keyed_persistent(&self, k: &Cid, block: &[u8]) -> anyhow::Result<()> { PersistentStore::put_keyed_persistent(&*self.db.read(), k, block) diff --git a/src/db/tests/mem_test.rs b/src/db/tests/mem_test.rs index c1d27f14d312..8a6c1101af43 100644 --- a/src/db/tests/mem_test.rs +++ b/src/db/tests/mem_test.rs @@ -34,3 +34,9 @@ fn mem_write_read_obj() { let db = MemoryDB::default(); subtests::write_read_obj(&db); } + +#[test] +fn mem_block_bloom_prune() { + let db = MemoryDB::default(); + subtests::block_bloom_prune(&db); +} diff --git a/src/db/tests/parity_test.rs b/src/db/tests/parity_test.rs index db7d7be7a2bc..f48d34ba6c1d 100644 --- a/src/db/tests/parity_test.rs +++ b/src/db/tests/parity_test.rs @@ -32,3 +32,9 @@ fn db_write_read_obj() { let db = TempParityDB::new(); subtests::write_read_obj(&*db); } + +#[test] +fn db_block_bloom_prune() { + let db = TempParityDB::new(); + subtests::block_bloom_prune(&*db); +} diff --git a/src/db/tests/subtests/mod.rs b/src/db/tests/subtests/mod.rs index de35af6d7982..382336ab99e2 100644 --- a/src/db/tests/subtests/mod.rs +++ b/src/db/tests/subtests/mod.rs @@ -1,7 +1,10 @@ // Copyright 2019-2026 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT -use crate::db::{SettingsStore, SettingsStoreExt}; +use crate::db::{EthBlockBloomStore, SettingsStore, SettingsStoreExt}; +use crate::utils::multihash::prelude::*; +use cid::Cid; +use fvm_ipld_encoding::DAG_CBOR; pub fn write_bin(db: &DB) where @@ -64,3 +67,25 @@ where assert!(db.read_obj::(key).unwrap().is_none()); assert!(db.require_obj::(key).is_err()); } + +pub fn block_bloom_prune(db: &DB) +where + DB: EthBlockBloomStore, +{ + let a = Cid::new_v1(DAG_CBOR, MultihashCode::Blake2b256.digest(b"a")); + let b = Cid::new_v1(DAG_CBOR, MultihashCode::Blake2b256.digest(b"b")); + let missing = Cid::new_v1(DAG_CBOR, MultihashCode::Blake2b256.digest(b"missing")); + let bloom_a = [0x11; 256]; + let bloom_b = [0x22; 256]; + + db.write_bloom(&a, 100, &bloom_a).unwrap(); + db.write_bloom(&b, 200, &bloom_b).unwrap(); + assert_eq!(db.read_bloom(&a).unwrap(), Some(bloom_a)); + assert_eq!(db.read_bloom(&b).unwrap(), Some(bloom_b)); + assert_eq!(db.read_bloom(&missing).unwrap(), None); + + // Only entries at or above the cutoff survive. + db.delete_blooms_before_height(150).unwrap(); + assert_eq!(db.read_bloom(&a).unwrap(), None); + assert_eq!(db.read_bloom(&b).unwrap(), Some(bloom_b)); +} diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index 5eb0a0472215..bfedb28ae97c 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -22,7 +22,7 @@ use crate::blocks::{Tipset, TipsetKey}; use crate::chain::{ChainStore, compute_base_fee, index::ResolveNullTipset}; use crate::chain_sync::NodeSyncStatus; use crate::cid_collections::CidHashSet; -use crate::db::DbImpl; +use crate::db::{DbImpl, EthBlockBloomStore}; use crate::eth::{ EAMMethod, EVMMethod, EthChainId as EthChainIdType, EthEip1559TxArgs, EthLegacyEip155TxArgs, EthLegacyHomesteadTxArgs, parse_eth_transaction, @@ -583,6 +583,9 @@ impl Block { full_transactions.push(tx); } + let logs_bloom = + block_logs_bloom(state_manager, &tipset, &executed_messages).await?; + Ok(Arc::new(Block { hash: block_hash, number: block_number, @@ -596,6 +599,7 @@ impl Block { .into(), gas_used: EthUint64(gas_used), transactions: Transactions::Full(full_transactions), + logs_bloom, ..Block::new(has_transactions, tipset.len()) })) }) @@ -1417,10 +1421,7 @@ async fn new_eth_tx_receipt( let mut bloom = Bloom::default(); for log in tx_receipt.logs.iter() { - for topic in log.topics.iter() { - bloom.accrue(topic.0.as_bytes()); - } - bloom.accrue(log.address.0.as_bytes()); + accrue_eth_log(&mut bloom, &log.address, &log.topics); } tx_receipt.logs_bloom = bloom.into(); @@ -3328,6 +3329,68 @@ fn eth_filter_logs_from_events( Ok(logs) } +/// Accrues a single Ethereum log's address and topics into `bloom` using the standard `M3:2048` scheme. +fn accrue_eth_log(bloom: &mut Bloom, address: &EthAddress, topics: &[EthHash]) { + for topic in topics { + bloom.accrue(topic.0.as_bytes()); + } + bloom.accrue(address.0.as_bytes()); +} + +async fn compute_block_logs_bloom( + state_manager: &StateManager, + tipset: &Tipset, + executed_messages: &[ExecutedMessage], +) -> Result { + let mut events = vec![]; + EthEventHandler::collect_events_from_messages( + state_manager, + tipset, + executed_messages, + None::<&ParsedFilter>, + SkipEvent::OnUnresolvedAddress, + EventRevertStatus::Applied, + &mut events, + ) + .await?; + + let mut bloom = Bloom::default(); + for event in &events { + let Some((_data, topics)) = eth_log_from_event(&event.entries) else { + continue; + }; + let Ok(address) = EthAddress::from_filecoin_address(&event.emitter_addr) else { + continue; + }; + accrue_eth_log(&mut bloom, &address, &topics); + } + Ok(bloom) +} + +/// Returns the block's logs bloom, reading it from the store when the chain indexer is enabled +/// and computing (then storing) it on a miss. +async fn block_logs_bloom( + state_manager: &StateManager, + tipset: &Tipset, + executed_messages: &[ExecutedMessage], +) -> Result { + let config = state_manager.chain_config(); + let store_bloom = config.enable_indexer && !config.is_devnet(); + + let key = tipset.key().cid()?; + if store_bloom && let Some(bloom) = state_manager.db().read_bloom(&key)? { + return Ok(Bloom(ethereum_types::Bloom(bloom))); + } + + let bloom = compute_block_logs_bloom(state_manager, tipset, executed_messages).await?; + if store_bloom { + state_manager + .db() + .write_bloom(&key, tipset.epoch(), &bloom.0.0)?; + } + Ok(bloom) +} + fn eth_filter_result_from_events( ctx: &Ctx, events: &[CollectedEvent], @@ -4676,6 +4739,46 @@ mod test { assert!(eth_log_from_event(&entries).is_none()); } + #[test] + fn test_accrue_eth_log_and_block_bloom_decomposition() { + let empty = Bloom::default(); + let full = Bloom(ethereum_types::Bloom(FULL_BLOOM)); + + // No logs yields the all-zeros bloom — the "definitely no events here" case + // indexers rely on. + assert_eq!(empty.0.0, EMPTY_BLOOM); + + let addr_a = EthAddress(ethereum_types::H160::from_slice(&[0x11; ADDRESS_LENGTH])); + let topic_a = EthHash(ethereum_types::H256::from_slice(&[0x22; EVM_WORD_LENGTH])); + let addr_b = EthAddress(ethereum_types::H160::from_slice(&[0x33; ADDRESS_LENGTH])); + let topic_b = EthHash(ethereum_types::H256::from_slice(&[0x44; EVM_WORD_LENGTH])); + + // A real log sets some bits, but not all of them. + let mut bloom_a = empty.clone(); + accrue_eth_log(&mut bloom_a, &addr_a, std::slice::from_ref(&topic_a)); + assert_ne!(bloom_a, empty); + assert_ne!(bloom_a, full); + + let mut bloom_b = empty.clone(); + accrue_eth_log(&mut bloom_b, &addr_b, std::slice::from_ref(&topic_b)); + + // The block bloom (both logs) equals the bitwise OR of the two individual + // (receipt) blooms. + let mut combined = bloom_a.clone(); + accrue_eth_log(&mut combined, &addr_b, std::slice::from_ref(&topic_b)); + + let mut expected = bloom_a.0.0; + for (out, b) in expected.iter_mut().zip(bloom_b.0.0.iter()) { + *out |= *b; + } + assert_eq!(combined.0.0, expected); + + // Accruing the same log twice equals accruing it once. + let mut twice = bloom_a.clone(); + accrue_eth_log(&mut twice, &addr_a, std::slice::from_ref(&topic_a)); + assert_eq!(twice, bloom_a); + } + #[test] fn test_from_bytes_valid() { let zero_bytes = [0u8; 32]; diff --git a/src/rpc/methods/eth/filter/mod.rs b/src/rpc/methods/eth/filter/mod.rs index cb35708aa349..4ef21f29b6ad 100644 --- a/src/rpc/methods/eth/filter/mod.rs +++ b/src/rpc/methods/eth/filter/mod.rs @@ -373,6 +373,9 @@ impl EthEventHandler { .await } + /// Collects the tipset's events from already-loaded executed messages, keeping those that + /// match `spec` and tagging each with `revert_status`. Use [`Self::collect_events`] instead + /// wages sthen the executed messill need to be loaded. pub async fn collect_events_from_messages( state_manager: &StateManager, tipset: &Tipset, diff --git a/src/rpc/snapshots/forest__rpc__tests__rpc__v0.snap b/src/rpc/snapshots/forest__rpc__tests__rpc__v0.snap index 62602db8e7cb..8d0883826ecb 100644 --- a/src/rpc/snapshots/forest__rpc__tests__rpc__v0.snap +++ b/src/rpc/snapshots/forest__rpc__tests__rpc__v0.snap @@ -5,7 +5,7 @@ expression: spec openrpc: 1.3.2 info: title: forest - version: 0.33.7 + version: 0.33.8 methods: - name: Filecoin.AuthNew description: Creates a new JWT authentication token with the given permissions. diff --git a/src/rpc/snapshots/forest__rpc__tests__rpc__v1.snap b/src/rpc/snapshots/forest__rpc__tests__rpc__v1.snap index 569cf957063a..597e12ef7bcf 100644 --- a/src/rpc/snapshots/forest__rpc__tests__rpc__v1.snap +++ b/src/rpc/snapshots/forest__rpc__tests__rpc__v1.snap @@ -5,7 +5,7 @@ expression: spec openrpc: 1.3.2 info: title: forest - version: 0.33.7 + version: 0.33.8 methods: - name: Filecoin.AuthNew description: Creates a new JWT authentication token with the given permissions. diff --git a/src/rpc/snapshots/forest__rpc__tests__rpc__v2.snap b/src/rpc/snapshots/forest__rpc__tests__rpc__v2.snap index c50b6d763fcd..8602812f2ddb 100644 --- a/src/rpc/snapshots/forest__rpc__tests__rpc__v2.snap +++ b/src/rpc/snapshots/forest__rpc__tests__rpc__v2.snap @@ -5,7 +5,7 @@ expression: spec openrpc: 1.3.2 info: title: forest - version: 0.33.7 + version: 0.33.8 methods: - name: Filecoin.ChainGetTipSet description: Returns the tipset with the specified CID. diff --git a/src/tool/subcommands/api_cmd/generate_test_snapshot.rs b/src/tool/subcommands/api_cmd/generate_test_snapshot.rs index 2a55a1ae95a6..dfb77c4be186 100644 --- a/src/tool/subcommands/api_cmd/generate_test_snapshot.rs +++ b/src/tool/subcommands/api_cmd/generate_test_snapshot.rs @@ -9,8 +9,9 @@ use crate::{ chain_sync::{SyncStatusReport, network_context::SyncNetworkContext}, daemon::{bundle::load_actor_bundles, db_util::load_all_forest_cars}, db::{ - CAR_DB_DIR_NAME, EthMappingsStore, HeaviestTipsetKeyProvider, MemoryDB, SettingsStore, - SettingsStoreExt, db_engine::open_db, parity_db::ParityDb, + BLOCK_BLOOM_LEN, CAR_DB_DIR_NAME, EthBlockBloomStore, EthMappingsStore, + HeaviestTipsetKeyProvider, MemoryDB, SettingsStore, SettingsStoreExt, db_engine::open_db, + parity_db::ParityDb, }, genesis::read_genesis_header, libp2p::{NetworkMessage, PeerManager}, @@ -18,7 +19,7 @@ use crate::{ message_pool::{MessagePool, MpoolLocker, NonceTracker}, networks::ChainConfig, prelude::*, - shim::address::CurrentNetwork, + shim::{address::CurrentNetwork, clock::ChainEpoch}, state_manager::StateManager, }; use api_compare_tests::TestDump; @@ -345,3 +346,22 @@ impl EthMappingsStore for ReadOpsTrackingStore { self.inner.set_tipset_key_at_epoch(ts) } } + +impl EthBlockBloomStore for ReadOpsTrackingStore { + fn read_bloom(&self, key: &Cid) -> anyhow::Result> { + self.inner.read_bloom(key) + } + + fn write_bloom( + &self, + key: &Cid, + height: ChainEpoch, + bloom: &[u8; BLOCK_BLOOM_LEN], + ) -> anyhow::Result<()> { + self.inner.write_bloom(key, height, bloom) + } + + fn delete_blooms_before_height(&self, height: ChainEpoch) -> anyhow::Result<()> { + self.inner.delete_blooms_before_height(height) + } +} diff --git a/src/tool/subcommands/api_cmd/test_snapshots.txt b/src/tool/subcommands/api_cmd/test_snapshots.txt index d9d2f5ddb575..e4a1b60a7feb 100644 --- a/src/tool/subcommands/api_cmd/test_snapshots.txt +++ b/src/tool/subcommands/api_cmd/test_snapshots.txt @@ -66,10 +66,10 @@ filecoin_ethgetbalance_v2_latest_1770291948779489.rpcsnap.json.zst filecoin_ethgetbalance_v2_pending_1770291949065157.rpcsnap.json.zst filecoin_ethgetbalance_v2_safe_1770291948803158.rpcsnap.json.zst filecoin_ethgetbalance_v2_unknown_addr_1770287845559744.rpcsnap.json.zst -filecoin_ethgetblockbyhash_1781166100013912.rpcsnap.json.zst -filecoin_ethgetblockbynumber_1737446676696328.rpcsnap.json.zst +filecoin_ethgetblockbyhash_1781886021508619.rpcsnap.json.zst +filecoin_ethgetblockbynumber_1781887507408523.rpcsnap.json.zst filecoin_ethgetblockbynumber_null_round_1782840028159441.rpcsnap.json.zst -filecoin_ethgetblockbynumber_v2_1781166100024105.rpcsnap.json.zst +filecoin_ethgetblockbynumber_v2_1781887507413486.rpcsnap.json.zst filecoin_ethgetblockreceipts_1781166100049979.rpcsnap.json.zst filecoin_ethgetblockreceipts_v2_1781166100038659.rpcsnap.json.zst filecoin_ethgetblockreceiptslimited_1781166100061549.rpcsnap.json.zst